feat(stage-capcitor): added Capacitor of stage for iOS (#845)
--------- Co-authored-by: Makito <i@maki.to>
@@ -0,0 +1,7 @@
|
||||
<h1 align="center">アイリ VTuber</h1>
|
||||
|
||||
<p align="center">
|
||||
[<a href="https://airi.ayaka.io">Try it</a>]
|
||||
</p>
|
||||
|
||||
> Heavily inspired by [Neuro-sama](https://www.youtube.com/@Neurosama)
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { CapacitorConfig } from '@capacitor/cli'
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'ai.moeru.airi',
|
||||
appName: 'AIRI',
|
||||
webDir: 'dist',
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -0,0 +1,58 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>AIRI</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" sizes="any" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<meta name="description" content="Open source attempt to create cyber companion.">
|
||||
<meta name="apple-mobile-web-app-title" content="AIRI" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="theme-color" content="rgb(18,18,18)" id="themeColor" />
|
||||
|
||||
<!-- Open Graph Protocol -->
|
||||
<meta property="og:title" content="AIRI">
|
||||
<meta property="og:site_name" content="AIRI">
|
||||
<meta property="og:image" content="https://airi.moeru.ai/open-graph.png">
|
||||
<meta property="og:description" content="Top 1 open source attempt to create cyber companion, capable of playing Minecraft, Factorio, Balatro, etc. .">
|
||||
<meta property="og:url" content="https://airi.moeru.ai/docs/en/">
|
||||
|
||||
<!-- Open Graph Protocol extension, Twitter cards -->
|
||||
<meta name="twitter:title" content="AIRI">
|
||||
<meta name="twitter:description" content="Top 1 open source attempt to create cyber companion, capable of playing Minecraft, Factorio, Balatro, etc. .">
|
||||
<meta name="twitter:image" content="https://airi.moeru.ai/open-graph.png">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:creator" content="Moeru AI">
|
||||
<script>
|
||||
;(function () {
|
||||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
const setting = localStorage.getItem('vueuse-color-scheme') || 'auto'
|
||||
if (setting === 'light' || (prefersDark && setting !== 'dark'))
|
||||
document.querySelector('#themeColor')?.setAttribute('content', 'rgb(255,255,255)')
|
||||
})()
|
||||
</script>
|
||||
<script>
|
||||
;(function () {
|
||||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
const setting = localStorage.getItem('vueuse-color-scheme') || 'auto'
|
||||
if (setting === 'dark' || (prefersDark && setting !== 'light'))
|
||||
document.documentElement.classList.toggle('dark', true)
|
||||
})()
|
||||
</script>
|
||||
<!-- Privacy-friendly analytics by Plausible -->
|
||||
<script async src="https://moeru-ai-airi-helper.kwaa.workers.dev/remote-assets/page-external-data/js/script.js"></script>
|
||||
<script>
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init({
|
||||
endpoint: "https://moeru-ai-airi-helper.kwaa.workers.dev/api/v1/page-external-data/submit"
|
||||
})
|
||||
</script>
|
||||
<script src="/assets/js/CubismSdkForWeb-5-r.3/Core/live2dcubismcore.min.js"></script>
|
||||
</head>
|
||||
<body class="font-sans">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<noscript> This website requires JavaScript to function properly. Please enable JavaScript to continue. </noscript>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
App/build
|
||||
App/Pods
|
||||
App/output
|
||||
App/App/public
|
||||
DerivedData
|
||||
xcuserdata
|
||||
|
||||
# Cordova plugins for Capacitor
|
||||
capacitor-cordova-ios-plugins
|
||||
|
||||
# Generated Config files
|
||||
App/App/capacitor.config.json
|
||||
App/App/config.xml
|
||||
@@ -0,0 +1,378 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 60;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
|
||||
4D22ABE92AF431CB00220026 /* CapApp-SPM in Frameworks */ = {isa = PBXBuildFile; productRef = 4D22ABE82AF431CB00220026 /* CapApp-SPM */; };
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||
958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
504EC3011FED79650016851F /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4D22ABE92AF431CB00220026 /* CapApp-SPM in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
504EC2FB1FED79650016851F = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
958DCC722DB07C7200EA8C5F /* debug.xcconfig */,
|
||||
504EC3061FED79650016851F /* App */,
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3051FED79650016851F /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504EC3041FED79650016851F /* App.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3061FED79650016851F /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */,
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */,
|
||||
504EC30B1FED79650016851F /* Main.storyboard */,
|
||||
504EC30E1FED79650016851F /* Assets.xcassets */,
|
||||
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
|
||||
504EC3131FED79650016851F /* Info.plist */,
|
||||
2FAD9762203C412B000D30F8 /* config.xml */,
|
||||
50B271D01FEDC1A000F3C39B /* public */,
|
||||
);
|
||||
path = App;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
504EC3031FED79650016851F /* App */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
|
||||
buildPhases = (
|
||||
504EC3001FED79650016851F /* Sources */,
|
||||
504EC3011FED79650016851F /* Frameworks */,
|
||||
504EC3021FED79650016851F /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = App;
|
||||
packageProductDependencies = (
|
||||
4D22ABE82AF431CB00220026 /* CapApp-SPM */,
|
||||
);
|
||||
productName = App;
|
||||
productReference = 504EC3041FED79650016851F /* App.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
504EC2FC1FED79650016851F /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0920;
|
||||
LastUpgradeCheck = 0920;
|
||||
TargetAttributes = {
|
||||
504EC3031FED79650016851F = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
LastSwiftMigration = 1100;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
|
||||
compatibilityVersion = "Xcode 8.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 504EC2FB1FED79650016851F;
|
||||
packageReferences = (
|
||||
D4C12C0A2AAA248700AAC8A2 /* XCLocalSwiftPackageReference "CapApp-SPM" */,
|
||||
);
|
||||
productRefGroup = 504EC3051FED79650016851F /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
504EC3031FED79650016851F /* App */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
504EC3021FED79650016851F /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */,
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */,
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
504EC3001FED79650016851F /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
504EC30B1FED79650016851F /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
504EC30C1FED79650016851F /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3101FED79650016851F /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
504EC3111FED79650016851F /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
504EC3141FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 958DCC722DB07C7200EA8C5F /* debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
504EC3151FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
504EC3171FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 958DCC722DB07C7200EA8C5F /* debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = R3M9XV85VN;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = ai.moeru.airi;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
504EC3181FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = R3M9XV85VN;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = ai.moeru.airi;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
504EC3141FED79650016851F /* Debug */,
|
||||
504EC3151FED79650016851F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
504EC3171FED79650016851F /* Debug */,
|
||||
504EC3181FED79650016851F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
D4C12C0A2AAA248700AAC8A2 /* XCLocalSwiftPackageReference "CapApp-SPM" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = "CapApp-SPM";
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
4D22ABE82AF431CB00220026 /* CapApp-SPM */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D4C12C0A2AAA248700AAC8A2 /* XCLocalSwiftPackageReference "CapApp-SPM" */;
|
||||
productName = "CapApp-SPM";
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 504EC2FC1FED79650016851F /* Project object */;
|
||||
}
|
||||
@@ -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,15 @@
|
||||
{
|
||||
"originHash" : "dcf9f6e21852e11ae14bb56853ac46203932d4fb843c3f466bc75208582c5e27",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "capacitor-swift-pm",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ionic-team/capacitor-swift-pm.git",
|
||||
"state" : {
|
||||
"revision" : "596259033e94829dffc552a40e7129262122995e",
|
||||
"version" : "8.0.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import UIKit
|
||||
import Capacitor
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
// Called when the app was launched with a url. Feel free to add additional processing here,
|
||||
// but if you want the App API to support tracking app url opens, make sure to keep this call
|
||||
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
||||
// Called when the app was launched with an activity, including Universal Links.
|
||||
// Feel free to add additional processing here, but if you want the App API to support
|
||||
// tracking app url opens, make sure to keep this call
|
||||
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||
}
|
||||
|
||||
}
|
||||
|
After Width: | Height: | Size: 108 KiB |
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "AppIcon-512@2x.png",
|
||||
"idiom": "universal",
|
||||
"platform": "ios",
|
||||
"size": "1024x1024"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "splash-2732x2732-2.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "splash-2732x2732-1.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "splash-2732x2732.png",
|
||||
"scale": "3x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17132" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17105"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<imageView key="view" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Splash" id="snD-IY-ifK">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</imageView>
|
||||
</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="Splash" width="1366" height="1366"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14111" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Bridge View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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>CAPACITOR_DEBUG</key>
|
||||
<string>$(CAPACITOR_DEBUG)</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>AIRI</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>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,9 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
@@ -0,0 +1,25 @@
|
||||
// swift-tools-version: 5.9
|
||||
import PackageDescription
|
||||
|
||||
// DO NOT MODIFY THIS FILE - managed by Capacitor CLI commands
|
||||
let package = Package(
|
||||
name: "CapApp-SPM",
|
||||
platforms: [.iOS(.v15)],
|
||||
products: [
|
||||
.library(
|
||||
name: "CapApp-SPM",
|
||||
targets: ["CapApp-SPM"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.0.0")
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "CapApp-SPM",
|
||||
dependencies: [
|
||||
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
||||
.product(name: "Cordova", package: "capacitor-swift-pm")
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
# CapApp-SPM
|
||||
|
||||
This SPM is used to host SPM dependencies for you Capacitor project
|
||||
|
||||
Do not modify the contents of it or there may be unintended consequences.
|
||||
@@ -0,0 +1 @@
|
||||
public let isCapacitorApp = true
|
||||
@@ -0,0 +1 @@
|
||||
CAPACITOR_DEBUG = true
|
||||
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"name": "@proj-airi/stage-capacitor",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"description": "LLM powered virtual character",
|
||||
"author": {
|
||||
"name": "Moeru AI Project AIRI Team",
|
||||
"email": "airi@moeru.ai",
|
||||
"url": "https://github.com/moeru-ai"
|
||||
},
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite --port 5273",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"dev:ios": "cap run ios -l --port 5273"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor/core": "catalog:",
|
||||
"@capacitor/ios": "catalog:",
|
||||
"@date-fns/utc": "^2.1.1",
|
||||
"@formkit/auto-animate": "^0.9.0",
|
||||
"@huggingface/transformers": "^3.8.1",
|
||||
"@moeru/eventa": "^1.0.0-alpha.10",
|
||||
"@moeru/std": "catalog:",
|
||||
"@proj-airi/audio": "workspace:^",
|
||||
"@proj-airi/ccc": "workspace:^",
|
||||
"@proj-airi/drizzle-duckdb-wasm": "catalog:",
|
||||
"@proj-airi/font-cjkfonts-allseto": "workspace:^",
|
||||
"@proj-airi/font-xiaolai": "workspace:^",
|
||||
"@proj-airi/i18n": "workspace:^",
|
||||
"@proj-airi/server-sdk": "workspace:^",
|
||||
"@proj-airi/stage-ui": "workspace:^",
|
||||
"@proj-airi/stage-ui-three": "workspace:^",
|
||||
"@proj-airi/stage-ui-three-performance-runtime": "workspace:^",
|
||||
"@proj-airi/ui": "workspace:^",
|
||||
"@proj-airi/ui-transitions": "workspace:^",
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"@stdlib/string-base-kebabcase": "^0.2.2",
|
||||
"@tresjs/cientos": "^5.2.1",
|
||||
"@tresjs/core": "^5.2.1",
|
||||
"@valibot/to-json-schema": "1.0.0-rc.0",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"@vueuse/shared": "^14.1.0",
|
||||
"@xsai-ext/providers": "catalog:",
|
||||
"@xsai-transformers/embed": "^0.0.11",
|
||||
"@xsai/generate-speech": "catalog:",
|
||||
"@xsai/generate-text": "catalog:",
|
||||
"@xsai/model": "catalog:",
|
||||
"@xsai/shared": "catalog:",
|
||||
"@xsai/shared-chat": "catalog:",
|
||||
"@xsai/stream-text": "catalog:",
|
||||
"@xsai/stream-transcription": "0.4.0-beta.8",
|
||||
"@xsai/utils-chat": "catalog:",
|
||||
"animejs": "^4.2.2",
|
||||
"colorjs.io": "^0.6.0",
|
||||
"culori": "^4.0.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "^3.3.1",
|
||||
"driver.js": "^1.4.0",
|
||||
"drizzle-kit": "^0.31.8",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"gpuu": "^1.0.6",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jszip": "^3.10.1",
|
||||
"localforage": "^1.10.0",
|
||||
"mediabunny": "^1.27.2",
|
||||
"nanoid": "^5.1.6",
|
||||
"node-vibrant": "^4.0.3",
|
||||
"nprogress": "^0.2.0",
|
||||
"ofetch": "^1.5.1",
|
||||
"onnxruntime-web": "^1.23.2",
|
||||
"pinia": "^3.0.4",
|
||||
"posthog-js": "catalog:",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"reka-ui": "^2.7.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.2",
|
||||
"shiki": "^3.20.0",
|
||||
"three": "^0.182.0",
|
||||
"unified": "^11.0.5",
|
||||
"unspeech": "^0.1.7",
|
||||
"uuid": "^13.0.0",
|
||||
"valibot": "^1.2.0",
|
||||
"vaul-vue": "^0.4.1",
|
||||
"vue": "^3.5.25",
|
||||
"vue-demi": "^0.14.10",
|
||||
"vue-i18n": "^11.2.2",
|
||||
"vue-router": "^4.6.4",
|
||||
"vue-sonner": "^2.0.9",
|
||||
"workbox-window": "^7.4.0",
|
||||
"xsschema": "catalog:",
|
||||
"yauzl": "^3.2.0",
|
||||
"zod": "^4.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/cli": "catalog:",
|
||||
"@iconify-json/carbon": "^1.2.15",
|
||||
"@iconify-json/eos-icons": "^1.2.4",
|
||||
"@iconify-json/lucide": "^1.2.82",
|
||||
"@iconify-json/mingcute": "^1.2.6",
|
||||
"@iconify-json/ph": "^1.2.2",
|
||||
"@iconify-json/simple-icons": "^1.2.63",
|
||||
"@iconify-json/solar": "^1.2.5",
|
||||
"@iconify-json/svg-spinners": "^1.2.4",
|
||||
"@iconify-json/vscode-icons": "^1.2.37",
|
||||
"@intlify/unplugin-vue-i18n": "^11.0.1",
|
||||
"@proj-airi/lobe-icons": "^1.0.18",
|
||||
"@proj-airi/unplugin-fetch": "^0.2.1",
|
||||
"@proj-airi/unplugin-live2d-sdk": "^0.1.6",
|
||||
"@proj-airi/vite-plugin-warpdrive": "workspace:*",
|
||||
"@shikijs/markdown-it": "^3.20.0",
|
||||
"@types/audioworklet": "catalog:",
|
||||
"@types/culori": "^4.0.1",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/three": "^0.182.0",
|
||||
"@types/yauzl": "^2.10.3",
|
||||
"@unocss/reset": "^66.5.11",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"@vue-macros/volar": "^3.1.1",
|
||||
"@vueuse/motion": "^3.0.3",
|
||||
"csstype": "^3.2.3",
|
||||
"hfup": "^1.0.0",
|
||||
"less": "^4.5.1",
|
||||
"unplugin-info": "^1.2.4",
|
||||
"unplugin-vue-router": "^0.19.0",
|
||||
"unplugin-yaml": "^3.0.7",
|
||||
"vite": "catalog:rolldown-vite",
|
||||
"vite-bundle-visualizer": "^1.2.1",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vite-plugin-vue-devtools": "^8.0.5",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vue-macros": "^3.1.1",
|
||||
"vue-tsc": "^3.1.8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
**/node_modules
|
||||
**/.DS_Store
|
||||
**/.git
|
||||
@@ -0,0 +1,4 @@
|
||||
/assets/*
|
||||
cache-control: max-age=31536000
|
||||
cache-control: immutable
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Plausible.io analytics
|
||||
#
|
||||
# Originally proxying Plausible through Netlify | Plausible docs
|
||||
# https://plausible.io/docs/proxy/guides/netlify
|
||||
#
|
||||
# /remote-assets/page-external-data/js/script.js https://plausible.io/js/script.js 200
|
||||
# /api/v1/page-external-data/submit https://plausible.io/api/event 200
|
||||
|
||||
# i18n
|
||||
/docs/ /docs/en/ 301
|
||||
|
||||
# Normalize old zh-hans locale path to zh-Hans
|
||||
# This ensures legacy links like /docs/zh-hans/ still work
|
||||
/docs/zh-hans/* /docs/zh-Hans/:splat 301
|
||||
/docs/zh-hans /docs/zh-Hans/ 301
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,61 @@
|
||||
<svg width="1500" height="1500" viewBox="0 0 1500 1500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2718_972)">
|
||||
<path d="M442.5 953.5L669 697L1080 927L985.5 1134.5L669 1104.5L442.5 953.5Z" fill="white"/>
|
||||
<path d="M985.5 727L1034.5 602.5L1121.5 629L1166.5 844L985.5 727Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1540.03 1958.88C1518.27 1990.95 1454.98 2031.91 1419.76 2052.52C1403.15 2062.26 1385.11 2059.4 1360.48 2063.41C1254.24 2080.59 1148.57 2057.97 1062.08 2032.19C1041.18 2025.89 989.344 1986.37 967.008 1994.68C934.934 2009 939.516 2047.66 923.193 2077.44C912.311 2100.35 883.674 2183.68 874.797 2202.3C852.174 2249.83 823.823 2298.8 796.618 2342.9C775.713 2376.98 722.162 2440.55 662.311 2407.05C634.247 2391.3 635.679 2369.53 631.383 2346.91C631.097 2346.05 631.097 2333.74 630.238 2333.74C627.088 2333.74 627.088 2333.74 623.365 2333.74C616.11 2342.52 608.76 2351.3 601.315 2360.08C559.791 2398.17 497.363 2389.01 482.758 2332.02C473.021 2294.51 491.062 2256.71 501.372 2232.08C520.654 2172.7 539.936 2113.33 559.218 2053.96C569.528 2004.7 546.045 1964.61 532.586 1932.25C507.385 1870.39 500.226 1796.8 474.739 1733.8C464.144 1707.16 443.811 1693.42 435.793 1674.52C426.343 1651.89 405.438 1559.11 388.829 1551.09C335.564 1527.04 321.246 1607.22 290.604 1618.1C228.176 1640.44 200.971 1552.52 196.961 1507.28C145.129 1490.1 106.183 1412.2 131.097 1340.32C153.147 1277.61 199.539 1232.36 287.454 1234.08C295.186 1224.35 307.214 1219.48 315.518 1210.6L332.7 1141.87C341.673 1125.26 350.551 1108.56 359.333 1091.76C371.647 1073.72 393.983 1050.81 412.311 1038.78C422.811 1033.62 433.216 1028.37 443.525 1023.03C437.798 1021.69 432.643 1020.07 428.061 1018.16C398.852 1005.27 393.983 980.359 373.365 956.017C371.36 961.172 348.451 999.545 335.85 1010.71C331.65 1008.04 327.45 1005.37 323.25 1002.7C315.805 990.954 315.518 909.053 309.218 884.139C259.104 683.108 276.572 507.563 421.761 407.621C433.216 399.889 441.234 376.407 445.243 370.106C472.735 350.824 500.322 331.542 528.004 312.26C557.786 297.655 598.737 295.364 641.979 289.064C658.588 288.491 675.293 287.918 692.093 287.346C818.955 286.2 964.717 357.22 1037.17 415.353C1053.78 436.735 1070.48 458.117 1087.28 479.499C1121.36 525.032 1159.45 565.983 1190.38 615.525C1219.59 662.489 1281.73 738.377 1280.87 801.378C1249.56 767.396 1218.34 733.509 1187.23 699.717C1195.82 736.086 1215.29 743.531 1229.32 769.305C1233.62 777.323 1239.34 786.773 1245.07 791.928C1252.52 796.701 1260.06 801.569 1267.7 806.533C1285.64 817.797 1303.59 829.06 1321.53 840.324C1379.38 882.993 1437.23 942.844 1477.6 1002.7C1548.05 1107.22 1598.74 1224.35 1630.81 1368.39C1677.78 1579.44 1628.23 1830.3 1540.03 1958.88ZM1048.05 716.899C1071.82 724.345 1106.18 762.146 1133.96 754.414C1133.96 754.414 1103.89 725.204 1090.43 710.599C1080.41 697.14 1079.55 686.258 1067.24 666.212L1059.79 657.048C1055.5 674.803 1040.6 702.294 1048.05 716.899ZM1005.95 984.081C1005.09 982.65 995.358 998.4 994.786 997.541C987.34 987.518 983.044 988.091 971.303 979.786C961.853 968.331 957.558 943.99 948.107 932.535C926.916 935.399 896.275 1010.14 879.379 1027.9C878.424 1027.32 877.374 1026.75 876.229 1026.18C863.056 978.641 766.549 821.138 732.471 801.378C706.507 796.605 680.543 791.928 654.579 787.346C629.665 814.551 618.21 854.643 592.151 880.989C565.518 907.621 530.009 925.09 504.522 952.867V965.181C513.399 978.354 527.718 981.218 542.036 988.663C561.891 998.018 581.651 1007.37 601.315 1016.73C607.328 1018.16 613.628 1018.45 619.642 1017.87C630.524 1017.01 641.12 1014.15 648.279 1012.15C680.066 1003.27 717.58 1011.57 746.79 1018.45C767.122 1023.31 778.577 1037.35 793.468 1046.51C821.532 1063.98 850.742 1069.71 876.229 1088.61C879.474 1092.81 882.624 1097.01 885.679 1101.21C913.743 1112.09 1004.52 1012.43 1005.95 984.081Z" fill="#F1D0DB"/>
|
||||
<path d="M534.877 418.215L635.965 159.91C635.965 159.91 712.712 199.715 801.773 395.305L534.877 418.215Z" fill="#F1D0DB"/>
|
||||
<path d="M197.82 483.511C197.82 483.511 265.117 459.17 335.277 475.493C405.724 491.816 279.722 635.86 279.722 635.86C279.722 635.86 184.934 508.139 197.82 483.511Z" fill="#F1D0DB"/>
|
||||
<path d="M531.154 378.41C531.154 378.41 528.863 423.084 540.89 424.229L636.251 160.197C636.251 159.911 635.965 159.911 635.965 160.197C513.685 382.706 535.736 351.492 531.154 378.41Z" fill="#EC92AD"/>
|
||||
<path d="M789.745 380.417C774.854 371.253 756.813 319.42 755.667 315.411C746.503 282.192 724.453 255.846 678.92 206.591C655.724 181.39 635.106 161.058 635.965 159.913C636.824 159.053 655.438 173.945 666.034 182.822C690.948 203.441 713.857 226.064 742.208 261.574C794.327 326.293 806.354 373.83 816.377 389.294C816.664 389.867 818.096 392.158 817.236 393.017C816.091 394.449 809.505 391.012 801.773 387.003C796.332 384.139 791.177 381.276 789.745 380.417Z" fill="#EC92AD"/>
|
||||
<path d="M566.95 414.495C564.373 412.777 576.973 386.717 596.446 356.935C612.483 332.307 614.201 334.311 629.092 309.97C637.397 296.797 644.843 283.624 646.561 284.483C647.706 284.77 646.274 289.065 645.129 295.079C639.115 324.289 646.847 338.893 643.983 359.798C643.983 360.944 643.411 364.38 644.556 364.667C645.702 365.239 648.852 359.798 652.002 355.216C655.152 350.921 660.306 346.339 670.329 337.461C690.089 319.707 692.093 320.279 692.953 320.852C694.671 321.998 692.666 325.72 692.953 339.466C692.953 340.612 692.953 351.207 697.534 359.798C698.107 360.657 698.966 362.089 700.398 362.948C702.403 364.094 705.266 364.38 712.139 361.23C722.448 356.362 723.88 351.494 727.317 352.353C730.181 353.212 729.608 356.935 734.762 366.385C738.485 373.544 751.085 375.549 749.94 377.84C748.508 380.703 731.612 381.562 729.894 381.849C670.329 391.585 601.887 408.767 582.987 410.486C581.555 410.772 577.832 411.058 573.25 412.49C568.668 413.922 567.523 415.068 566.95 414.495Z" fill="#EC92AD"/>
|
||||
<path d="M280.581 628.703C283.158 630.994 281.44 639.585 278.576 640.444C276.572 641.017 272.849 638.726 260.821 618.68C248.221 597.775 242.78 590.902 231.039 568.852C220.443 548.233 214.43 534.201 210.707 529.047C210.707 529.047 200.684 512.437 195.243 494.682C194.384 491.819 194.098 488.669 194.098 488.669C194.098 486.378 193.811 483.8 194.384 482.941C196.388 480.364 233.616 498.691 267.981 522.746C277.431 529.619 298.909 545.37 305.495 546.515C305.781 546.801 306.641 546.802 307.213 547.661C309.504 550.524 300.34 556.824 297.19 566.275C289.745 586.893 289.745 605.221 287.167 610.375C283.731 616.962 281.44 621.257 280.581 621.257C279.722 621.257 279.435 606.653 279.435 606.653C279.435 606.653 268.84 608.944 267.408 606.939C263.971 601.498 285.736 584.889 282.586 579.448C280.295 575.438 265.976 580.879 264.831 578.016C263.112 574.579 281.726 568.279 281.726 559.115C281.726 546.515 245.644 540.788 227.316 521.315C226.743 520.456 225.598 519.31 224.739 519.596C221.303 521.315 244.785 573.72 255.667 595.484C267.408 618.394 269.126 614.957 273.994 622.689C274.567 623.548 275.713 625.553 278.004 627.271C279.435 628.13 280.008 628.13 280.581 628.703Z" fill="#EC92AD"/>
|
||||
<path d="M1540.03 1958.88C1518.27 1990.95 1454.98 2031.9 1419.76 2052.52C1403.15 2062.26 1385.11 2059.39 1360.48 2063.4C1254.24 2080.58 1148.57 2057.96 1062.08 2032.19C1041.18 2025.89 989.344 1986.37 967.008 1994.67C934.934 2008.99 939.516 2047.65 923.193 2077.43C912.311 2100.34 883.674 2183.68 874.797 2202.29C852.174 2249.83 823.823 2298.8 796.618 2342.9C775.713 2376.98 722.162 2440.55 662.311 2407.05C634.247 2391.3 635.679 2369.53 631.383 2346.91C631.097 2346.05 631.097 2333.74 630.238 2333.74C627.088 2333.74 627.088 2333.74 623.365 2333.74C616.11 2342.52 608.76 2351.3 601.315 2360.08C559.791 2398.17 497.363 2389 482.758 2332.02C473.021 2294.5 491.062 2256.7 501.372 2232.07C520.654 2172.7 539.936 2113.33 559.218 2053.95C569.528 2004.7 546.045 1964.61 532.586 1932.25C507.385 1870.39 500.226 1796.79 474.739 1733.79C464.144 1707.16 443.811 1693.41 435.793 1674.51C426.343 1651.89 405.438 1559.11 388.829 1551.09C335.564 1527.03 321.246 1607.22 290.604 1618.1C228.176 1640.44 200.971 1552.52 196.961 1507.27C145.129 1490.09 106.183 1412.2 131.097 1340.32C153.147 1277.61 199.539 1232.36 287.454 1234.08C295.186 1224.34 307.214 1219.47 315.518 1210.6L332.7 1141.87C341.673 1125.26 350.551 1108.55 359.333 1091.75C371.647 1073.71 393.983 1050.8 412.311 1038.77C422.811 1033.62 433.216 1028.37 443.525 1023.02C437.798 1021.69 432.643 1020.07 428.061 1018.16C438.943 1015.01 448.68 1014.15 456.698 1014.15C497.076 1013.86 513.399 1036.77 557.786 1035.34C577.832 1034.48 577.832 1029.61 619.642 1017.87C682.643 1000.11 714.144 991.237 746.217 998.969C783.445 1007.85 777.431 1027.03 841.865 1056.24C875.656 1071.42 899.711 1082.02 925.484 1073.14C950.685 1064.26 968.726 1040.21 975.599 1031.04C985.908 1017.01 991.349 1004.7 993.067 1000.97C993.067 1000.97 996.217 994.101 1003.66 980.642C1015.4 959.737 1037.45 923.941 1074.68 874.399C1114.2 822.566 1204.41 809.107 1267.7 806.529C1285.64 817.793 1303.59 829.057 1321.53 840.321C1379.38 882.99 1437.23 942.841 1477.6 1002.69C1548.05 1107.22 1598.74 1224.34 1630.81 1368.39C1677.78 1579.44 1628.23 1830.3 1540.03 1958.88Z" fill="url(#paint0_linear_2718_972)"/>
|
||||
<path d="M402.861 438.549C404.579 468.618 379.665 549.946 374.797 567.988C373.651 568.274 372.219 568.56 370.501 568.847C366.778 569.133 363.056 568.274 359.906 565.983L359.333 565.697C352.746 518.159 378.52 470.336 402.861 438.549Z" fill="#EC92AD"/>
|
||||
<path d="M339.574 488.086C332.987 507.559 322.392 530.469 315.805 549.942C315.423 550.514 315.232 551.183 315.232 551.946C304.064 604.638 301.2 731.213 312.369 785.337C325.351 839.938 338.333 894.539 351.315 949.14C358.761 990.664 306.355 1080.3 284.018 1085.17C284.591 1084.31 287.455 1071.42 290.032 1060.82C291.368 1054.91 292.227 1048.99 292.609 1043.07C295.091 1005.46 297.478 967.849 299.769 930.24C300.914 915.921 299.482 901.603 296.046 887.571C287.836 854.543 279.723 821.515 271.704 788.487C257.672 716.036 278.864 597.479 299.769 546.219C308.073 525.887 338.715 488.086 339.574 488.086Z" fill="#EC92AD"/>
|
||||
<path d="M576.114 529.043C626.228 550.521 648.565 618.104 679.207 660.2C716.148 710.887 757.385 762.434 788.6 817.989C792.895 830.589 828.119 899.318 841.864 921.082C875.083 977.783 907.443 1088.89 907.443 1174.23C906.297 1173.66 816.377 968.047 815.232 968.047C820.386 1070.57 800.054 1107.22 800.054 1106.08C784.59 1036.49 782.013 973.774 760.535 921.082C705.266 785.63 650.57 646.741 576.114 529.043Z" fill="#EC92AD"/>
|
||||
<path d="M365.346 604.062C379.665 618.667 383.674 682.241 390.261 707.155C400.761 742.092 411.165 776.934 421.475 811.68C441.807 858.358 476.744 885.85 474.739 945.987C473.212 945.987 471.589 945.987 469.871 945.987C386.824 913.914 351.314 728.06 365.346 604.062Z" fill="#EC92AD"/>
|
||||
<path d="M1042.9 808.247C1042.32 854.925 992.495 949.427 971.876 983.791C968.44 984.364 934.362 971.763 931.212 960.309C928.348 949.14 987.054 802.233 994.786 795.646C1009.1 800.515 1025.71 807.101 1042.9 808.247Z" fill="#EC92AD"/>
|
||||
<path d="M1320.39 1447.71C1244.21 1448.28 1146.85 1396.45 1093.01 1365.23C1091.01 1364.09 1089.86 1361.51 1090.72 1359.22C1091.29 1356.93 1092.15 1354.64 1092.72 1352.63C1093.58 1350.34 1095.59 1349.2 1097.88 1349.2C1132.82 1350.63 1145.42 1371.25 1169.18 1381.84C1208.42 1399.6 1265.12 1417.64 1316.95 1419.36C1319.81 1419.36 1322.39 1417.07 1322.39 1413.92C1322.96 1338.32 1314.66 1270.73 1293.75 1219.47C1293.56 1219.28 1293.47 1219.09 1293.47 1218.9C1292.04 1212.6 1288.31 1206.59 1286.6 1200.29C1286.6 1199.71 1286.02 1198.85 1285.45 1198.28C1281.44 1193.99 1274.57 1194.27 1270.85 1188.26C1267.12 1182.24 1294.04 1155.33 1297.76 1137.57C1311.51 1071.42 1246.22 968.899 1208.42 935.394C1203.83 936.826 1202.69 937.399 1198.11 938.831C1195.24 956.299 1185.51 962.027 1177.2 974.341C1144.84 1020.73 1128.23 1072.28 1049.77 1072.57C1047.76 1072.57 1046.33 1074.57 1044.9 1076.29C1022.28 1106.64 994.785 1132.99 973.88 1153.89C920.616 1207.44 839.001 1343.47 764.831 1361.51C712.712 1374.4 647.993 1358.07 584.992 1367.81C551.773 1372.97 421.188 1389.29 392.552 1383.56C389.974 1382.99 388.256 1380.41 388.542 1377.83C388.829 1372.97 386.824 1369.53 392.552 1366.67C416.32 1360.94 437.511 1370.68 458.416 1366.38C489.153 1359.51 519.89 1352.73 550.627 1346.05C550.627 1346.05 550.723 1346.05 550.914 1346.05C633.101 1332.3 708.13 1339.17 783.731 1324.86C785.45 1324.57 793.468 1316.55 794.614 1315.12C818.669 1291.92 830.696 1278.46 847.305 1261C871.074 1236.08 896.275 1212.03 917.752 1185.11C946.676 1149.31 968.153 1128.69 1016.55 1075.14C1019.7 1071.71 1021.42 1069.99 1021.13 1067.98C1021.13 1066.27 1021.13 1063.97 1017.69 1062.26C982.472 1044.21 1000.51 990.95 1011.39 960.308C1025.71 920.217 1117.92 849.197 1181.78 863.229C1267.98 882.703 1306.35 937.685 1330.12 1021.02C1335.85 1040.78 1347.02 1070.85 1341.29 1098.91C1341.1 1099.1 1341.01 1099.29 1341.01 1099.48C1336.42 1111.51 1334.42 1128.69 1329.84 1140.43C1329.84 1140.63 1329.84 1140.72 1329.84 1140.72C1323.82 1162.77 1321.25 1178.24 1305.5 1191.69C1303.49 1193.41 1302.92 1196.28 1304.35 1198.28C1335.85 1252.98 1349.31 1322.57 1351.89 1402.18C1353.61 1446.56 1349.88 1510.71 1357.61 1547.94C1357.9 1549.66 1359.33 1551.09 1360.76 1551.66C1426.63 1580.3 1593.87 1723.48 1613.34 1789.92C1613.53 1790.87 1613.53 1791.73 1613.34 1792.5C1608.76 1811.97 1602.17 1830.3 1595.59 1848.05C1595.4 1848.62 1595.01 1849.2 1594.44 1849.77C1549.77 1898.74 1458.99 1955.73 1360.76 1912.49C1304.92 1887.86 1267.41 1854.35 1229.61 1809.68C1203.83 1779.32 1178.63 1744.96 1153.72 1713.17C1112.2 1660.48 1078.98 1602.92 1039.75 1547.36C1022.56 1523.31 1006.53 1495.25 988.199 1471.19C988.199 1471 988.104 1470.81 987.913 1470.62C975.885 1458.21 963.953 1445.8 952.117 1433.39C948.966 1427.66 950.971 1426.23 954.121 1423.37C954.885 1422.79 955.744 1422.41 956.698 1422.22C977.317 1418.79 998.508 1448.85 1008.53 1462.03C1008.53 1462.22 1008.63 1462.31 1008.82 1462.31C1027.43 1481.21 1042.9 1508.13 1060.08 1529.61C1060.27 1529.8 1060.36 1529.9 1060.36 1529.9C1076.4 1554.81 1092.15 1572.57 1108.47 1597.48C1170.62 1685.68 1238.77 1803.67 1330.12 1861.51C1354.46 1876.98 1403.43 1909.05 1445.82 1900.46C1493.07 1891.01 1524.57 1870.1 1557.5 1849.77C1573.25 1839.75 1578.69 1819.13 1569.53 1802.52C1521.13 1716.32 1422.91 1613.8 1332.41 1587.46C1330.12 1586.88 1328.69 1584.88 1328.41 1582.59C1327.45 1539.44 1326.5 1496.2 1325.54 1452.86C1325.54 1450 1323.25 1447.71 1320.39 1447.71Z" fill="#EC92AD"/>
|
||||
<path d="M562.082 1052.23C664.889 1048.51 591.005 1225.2 519.986 1200.86C472.162 1184.25 501.945 1104.64 521.418 1078.87C531.441 1065.98 547.191 1060.82 562.082 1052.23Z" fill="#EC92AD"/>
|
||||
<path d="M407.443 1066.27C486.194 1071.14 421.188 1213.46 365.346 1194.56C292.895 1169.93 373.365 1082.59 407.443 1066.27Z" fill="#EC92AD"/>
|
||||
<path d="M656.011 1106.94C689.516 1129.85 715.575 1222.06 691.807 1266.44C675.484 1270.17 603.319 1289.93 588.714 1271.02C588.714 1268.92 588.714 1266.82 588.714 1264.72C644.842 1243.53 657.729 1182.25 656.011 1106.94Z" fill="#EC92AD"/>
|
||||
<path d="M491.921 1117.81C489.058 1157.33 478.748 1214.6 474.739 1239.8C471.303 1260.14 481.039 1286.77 473.021 1300.8C463.284 1306.91 453.452 1313.11 443.525 1319.41C407.442 1314.55 398.565 1267.01 398.279 1227.2C424.911 1215.46 484.762 1149.88 491.921 1117.81Z" fill="#EC92AD"/>
|
||||
<path d="M201.256 1280.47C223.307 1279.9 256.812 1294.79 265.403 1308.53C187.224 1307.96 170.901 1389.29 159.16 1456.87C157.633 1456.3 156.105 1455.82 154.578 1455.44C106.755 1371.25 146.846 1330.87 201.256 1280.47Z" fill="#EC92AD"/>
|
||||
<path d="M398.279 1311.69C410.307 1315.41 418.611 1316.55 429.78 1324.57C434.075 1327.72 432.644 1330.59 428.062 1335.74C406.584 1354.07 376.229 1365.81 351.315 1363.23C336.71 1361.8 315.805 1360.65 306.355 1366.95C304.064 1368.1 304.35 1370.1 304.637 1371.82C312.082 1413.06 373.938 1518.16 357.329 1551.38C352.46 1551.66 351.028 1551.66 346.446 1551.66C326.401 1522.17 257.672 1407.62 292.036 1352.35C309.505 1324 371.933 1331.73 398.279 1311.69Z" fill="#EC92AD"/>
|
||||
<path d="M442.5 953.5L669 697L1080 927L985.5 1134.5L669 1104.5L442.5 953.5Z" fill="#FFEBEB"/>
|
||||
<path d="M985.5 727L1034.5 602.5L1121.5 629L1166.5 844L985.5 727Z" fill="#FFEBEB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1540.03 1958.88C1518.27 1990.95 1454.98 2031.91 1419.76 2052.52C1403.15 2062.26 1385.11 2059.4 1360.48 2063.41C1254.24 2080.59 1148.57 2057.97 1062.08 2032.19C1041.18 2025.89 989.344 1986.37 967.008 1994.68C934.934 2009 939.516 2047.66 923.193 2077.44C912.311 2100.35 883.674 2183.68 874.797 2202.3C852.174 2249.83 823.823 2298.8 796.618 2342.9C775.713 2376.98 722.162 2440.55 662.311 2407.05C634.247 2391.3 635.679 2369.53 631.383 2346.91C631.097 2346.05 631.097 2333.74 630.238 2333.74C627.088 2333.74 627.088 2333.74 623.365 2333.74C616.11 2342.52 608.76 2351.3 601.315 2360.08C559.791 2398.17 497.363 2389.01 482.758 2332.02C473.021 2294.51 491.062 2256.71 501.372 2232.08C520.654 2172.7 539.936 2113.33 559.218 2053.96C569.528 2004.7 546.045 1964.61 532.586 1932.25C507.385 1870.39 500.226 1796.8 474.739 1733.8C464.144 1707.16 443.811 1693.42 435.793 1674.52C426.343 1651.89 405.438 1559.11 388.829 1551.09C335.564 1527.04 321.246 1607.22 290.604 1618.1C228.176 1640.44 200.971 1552.52 196.961 1507.28C145.129 1490.1 106.183 1412.2 131.097 1340.32C153.147 1277.61 199.539 1232.36 287.454 1234.08C295.186 1224.35 307.214 1219.48 315.518 1210.6L332.7 1141.87C341.673 1125.26 350.551 1108.56 359.333 1091.76C371.647 1073.72 393.983 1050.81 412.311 1038.78C422.811 1033.62 433.216 1028.37 443.525 1023.03C437.798 1021.69 432.643 1020.07 428.061 1018.16C398.852 1005.27 393.983 980.359 373.365 956.017C371.36 961.172 348.451 999.545 335.85 1010.71C331.65 1008.04 327.45 1005.37 323.25 1002.7C315.805 990.954 315.518 909.053 309.218 884.139C259.104 683.108 276.572 507.563 421.761 407.621C433.216 399.889 441.234 376.407 445.243 370.106C472.735 350.824 500.322 331.542 528.004 312.26C557.786 297.655 598.737 295.364 641.979 289.064C658.588 288.491 675.293 287.918 692.093 287.346C818.955 286.2 964.717 357.22 1037.17 415.353C1053.78 436.735 1070.48 458.117 1087.28 479.499C1121.36 525.032 1159.45 565.983 1190.38 615.525C1219.59 662.489 1281.73 738.377 1280.87 801.378C1249.56 767.396 1218.34 733.509 1187.23 699.717C1195.82 736.086 1215.29 743.531 1229.32 769.305C1233.62 777.323 1239.34 786.773 1245.07 791.928C1252.52 796.701 1260.06 801.569 1267.7 806.533C1285.64 817.797 1303.59 829.06 1321.53 840.324C1379.38 882.993 1437.23 942.844 1477.6 1002.7C1548.05 1107.22 1598.74 1224.35 1630.81 1368.39C1677.78 1579.44 1628.23 1830.3 1540.03 1958.88ZM1048.05 716.899C1071.82 724.345 1106.18 762.146 1133.96 754.414C1133.96 754.414 1103.89 725.204 1090.43 710.599C1080.41 697.14 1079.55 686.258 1067.24 666.212L1059.79 657.048C1055.5 674.803 1040.6 702.294 1048.05 716.899ZM1005.95 984.081C1005.09 982.65 995.358 998.4 994.786 997.541C987.34 987.518 983.044 988.091 971.303 979.786C961.853 968.331 957.558 943.99 948.107 932.535C926.916 935.399 896.275 1010.14 879.379 1027.9C878.424 1027.32 877.374 1026.75 876.229 1026.18C863.056 978.641 766.549 821.138 732.471 801.378C706.507 796.605 680.543 791.928 654.579 787.346C629.665 814.551 618.21 854.643 592.151 880.989C565.518 907.621 530.009 925.09 504.522 952.867V965.181C513.399 978.354 527.718 981.218 542.036 988.663C561.891 998.018 581.651 1007.37 601.315 1016.73C607.328 1018.16 613.628 1018.45 619.642 1017.87C630.524 1017.01 641.12 1014.15 648.279 1012.15C680.066 1003.27 717.58 1011.57 746.79 1018.45C767.122 1023.31 778.577 1037.35 793.468 1046.51C821.532 1063.98 850.742 1069.71 876.229 1088.61C879.474 1092.81 882.624 1097.01 885.679 1101.21C913.743 1112.09 1004.52 1012.43 1005.95 984.081Z" fill="#F1D0DB"/>
|
||||
<path d="M534.877 418.215L635.965 159.91C635.965 159.91 712.712 199.715 801.773 395.305L534.877 418.215Z" fill="#F1D0DB"/>
|
||||
<path d="M197.82 483.511C197.82 483.511 265.117 459.17 335.277 475.493C405.724 491.816 279.722 635.86 279.722 635.86C279.722 635.86 184.934 508.139 197.82 483.511Z" fill="#F1D0DB"/>
|
||||
<path d="M531.154 378.41C531.154 378.41 528.863 423.084 540.89 424.229L636.251 160.197C636.251 159.911 635.965 159.911 635.965 160.197C513.685 382.706 535.736 351.492 531.154 378.41Z" fill="#EC92AD"/>
|
||||
<path d="M789.745 380.417C774.854 371.253 756.813 319.42 755.667 315.411C746.503 282.192 724.453 255.846 678.92 206.591C655.724 181.39 635.106 161.058 635.965 159.913C636.824 159.053 655.438 173.945 666.034 182.822C690.948 203.441 713.857 226.064 742.208 261.574C794.327 326.293 806.354 373.83 816.377 389.294C816.664 389.867 818.096 392.158 817.236 393.017C816.091 394.449 809.505 391.012 801.773 387.003C796.332 384.139 791.177 381.276 789.745 380.417Z" fill="#EC92AD"/>
|
||||
<path d="M566.95 414.495C564.373 412.777 576.973 386.717 596.446 356.935C612.483 332.307 614.201 334.311 629.092 309.97C637.397 296.797 644.843 283.624 646.561 284.483C647.706 284.77 646.274 289.065 645.129 295.079C639.115 324.289 646.847 338.893 643.983 359.798C643.983 360.944 643.411 364.38 644.556 364.667C645.702 365.239 648.852 359.798 652.002 355.216C655.152 350.921 660.306 346.339 670.329 337.461C690.089 319.707 692.093 320.279 692.953 320.852C694.671 321.998 692.666 325.72 692.953 339.466C692.953 340.612 692.953 351.207 697.534 359.798C698.107 360.657 698.966 362.089 700.398 362.948C702.403 364.094 705.266 364.38 712.139 361.23C722.448 356.362 723.88 351.494 727.317 352.353C730.181 353.212 729.608 356.935 734.762 366.385C738.485 373.544 751.085 375.549 749.94 377.84C748.508 380.703 731.612 381.562 729.894 381.849C670.329 391.585 601.887 408.767 582.987 410.486C581.555 410.772 577.832 411.058 573.25 412.49C568.668 413.922 567.523 415.068 566.95 414.495Z" fill="#EC92AD"/>
|
||||
<path d="M280.581 628.703C283.158 630.994 281.44 639.585 278.576 640.444C276.572 641.017 272.849 638.726 260.821 618.68C248.221 597.775 242.78 590.902 231.039 568.852C220.443 548.233 214.43 534.201 210.707 529.047C210.707 529.047 200.684 512.437 195.243 494.682C194.384 491.819 194.098 488.669 194.098 488.669C194.098 486.378 193.811 483.8 194.384 482.941C196.388 480.364 233.616 498.691 267.981 522.746C277.431 529.619 298.909 545.37 305.495 546.515C305.781 546.801 306.641 546.802 307.213 547.661C309.504 550.524 300.34 556.824 297.19 566.275C289.745 586.893 289.745 605.221 287.167 610.375C283.731 616.962 281.44 621.257 280.581 621.257C279.722 621.257 279.435 606.653 279.435 606.653C279.435 606.653 268.84 608.944 267.408 606.939C263.971 601.498 285.736 584.889 282.586 579.448C280.295 575.438 265.976 580.879 264.831 578.016C263.112 574.579 281.726 568.279 281.726 559.115C281.726 546.515 245.644 540.788 227.316 521.315C226.743 520.456 225.598 519.31 224.739 519.596C221.303 521.315 244.785 573.72 255.667 595.484C267.408 618.394 269.126 614.957 273.994 622.689C274.567 623.548 275.713 625.553 278.004 627.271C279.435 628.13 280.008 628.13 280.581 628.703Z" fill="#EC92AD"/>
|
||||
<path d="M1540.03 1958.88C1518.27 1990.95 1454.98 2031.9 1419.76 2052.52C1403.15 2062.26 1385.11 2059.39 1360.48 2063.4C1254.24 2080.58 1148.57 2057.96 1062.08 2032.19C1041.18 2025.89 989.344 1986.37 967.008 1994.67C934.934 2008.99 939.516 2047.65 923.193 2077.43C912.311 2100.34 883.674 2183.68 874.797 2202.29C852.174 2249.83 823.823 2298.8 796.618 2342.9C775.713 2376.98 722.162 2440.55 662.311 2407.05C634.247 2391.3 635.679 2369.53 631.383 2346.91C631.097 2346.05 631.097 2333.74 630.238 2333.74C627.088 2333.74 627.088 2333.74 623.365 2333.74C616.11 2342.52 608.76 2351.3 601.315 2360.08C559.791 2398.17 497.363 2389 482.758 2332.02C473.021 2294.5 491.062 2256.7 501.372 2232.07C520.654 2172.7 539.936 2113.33 559.218 2053.95C569.528 2004.7 546.045 1964.61 532.586 1932.25C507.385 1870.39 500.226 1796.79 474.739 1733.79C464.144 1707.16 443.811 1693.41 435.793 1674.51C426.343 1651.89 405.438 1559.11 388.829 1551.09C335.564 1527.03 321.246 1607.22 290.604 1618.1C228.176 1640.44 200.971 1552.52 196.961 1507.27C145.129 1490.09 106.183 1412.2 131.097 1340.32C153.147 1277.61 199.539 1232.36 287.454 1234.08C295.186 1224.34 307.214 1219.47 315.518 1210.6L332.7 1141.87C341.673 1125.26 350.551 1108.55 359.333 1091.75C371.647 1073.71 393.983 1050.8 412.311 1038.77C422.811 1033.62 433.216 1028.37 443.525 1023.02C437.798 1021.69 432.643 1020.07 428.061 1018.16C438.943 1015.01 448.68 1014.15 456.698 1014.15C497.076 1013.86 513.399 1036.77 557.786 1035.34C577.832 1034.48 577.832 1029.61 619.642 1017.87C682.643 1000.11 714.144 991.237 746.217 998.969C783.445 1007.85 777.431 1027.03 841.865 1056.24C875.656 1071.42 899.711 1082.02 925.484 1073.14C950.685 1064.26 968.726 1040.21 975.599 1031.04C985.908 1017.01 991.349 1004.7 993.067 1000.97C993.067 1000.97 996.217 994.101 1003.66 980.642C1015.4 959.737 1037.45 923.941 1074.68 874.399C1114.2 822.566 1204.41 809.107 1267.7 806.529C1285.64 817.793 1303.59 829.057 1321.53 840.321C1379.38 882.99 1437.23 942.841 1477.6 1002.69C1548.05 1107.22 1598.74 1224.34 1630.81 1368.39C1677.78 1579.44 1628.23 1830.3 1540.03 1958.88Z" fill="url(#paint1_linear_2718_972)"/>
|
||||
<path d="M402.861 438.549C404.579 468.618 379.665 549.946 374.797 567.988C373.651 568.274 372.219 568.56 370.501 568.847C366.778 569.133 363.056 568.274 359.906 565.983L359.333 565.697C352.746 518.159 378.52 470.336 402.861 438.549Z" fill="#EC92AD"/>
|
||||
<path d="M339.574 488.086C332.987 507.559 322.392 530.469 315.805 549.942C315.423 550.514 315.232 551.183 315.232 551.946C304.064 604.638 301.2 731.213 312.369 785.337C325.351 839.938 338.333 894.539 351.315 949.14C358.761 990.664 306.355 1080.3 284.018 1085.17C284.591 1084.31 287.455 1071.42 290.032 1060.82C291.368 1054.91 292.227 1048.99 292.609 1043.07C295.091 1005.46 297.478 967.849 299.769 930.24C300.914 915.921 299.482 901.603 296.046 887.571C287.836 854.543 279.723 821.515 271.704 788.487C257.672 716.036 278.864 597.479 299.769 546.219C308.073 525.887 338.715 488.086 339.574 488.086Z" fill="#EC92AD"/>
|
||||
<path d="M576.114 529.043C626.228 550.521 648.565 618.104 679.207 660.2C716.148 710.887 757.385 762.434 788.6 817.989C792.895 830.589 828.119 899.318 841.864 921.082C875.083 977.783 907.443 1088.89 907.443 1174.23C906.297 1173.66 816.377 968.047 815.232 968.047C820.386 1070.57 800.054 1107.22 800.054 1106.08C784.59 1036.49 782.013 973.774 760.535 921.082C705.266 785.63 650.57 646.741 576.114 529.043Z" fill="#EC92AD"/>
|
||||
<path d="M365.346 604.062C379.665 618.667 383.674 682.241 390.261 707.155C400.761 742.092 411.165 776.934 421.475 811.68C441.807 858.358 476.744 885.85 474.739 945.987C473.212 945.987 471.589 945.987 469.871 945.987C386.824 913.914 351.314 728.06 365.346 604.062Z" fill="#EC92AD"/>
|
||||
<path d="M1042.9 808.247C1042.32 854.925 992.495 949.427 971.876 983.791C968.44 984.364 934.362 971.763 931.212 960.309C928.348 949.14 987.054 802.233 994.786 795.646C1009.1 800.515 1025.71 807.101 1042.9 808.247Z" fill="#EC92AD"/>
|
||||
<path d="M1320.39 1447.71C1244.21 1448.28 1146.85 1396.45 1093.01 1365.23C1091.01 1364.09 1089.86 1361.51 1090.72 1359.22C1091.29 1356.93 1092.15 1354.64 1092.72 1352.63C1093.58 1350.34 1095.59 1349.2 1097.88 1349.2C1132.82 1350.63 1145.42 1371.25 1169.18 1381.84C1208.42 1399.6 1265.12 1417.64 1316.95 1419.36C1319.81 1419.36 1322.39 1417.07 1322.39 1413.92C1322.96 1338.32 1314.66 1270.73 1293.75 1219.47C1293.56 1219.28 1293.47 1219.09 1293.47 1218.9C1292.04 1212.6 1288.31 1206.59 1286.6 1200.29C1286.6 1199.71 1286.02 1198.85 1285.45 1198.28C1281.44 1193.99 1274.57 1194.27 1270.85 1188.26C1267.12 1182.24 1294.04 1155.33 1297.76 1137.57C1311.51 1071.42 1246.22 968.899 1208.42 935.394C1203.83 936.826 1202.69 937.399 1198.11 938.831C1195.24 956.299 1185.51 962.027 1177.2 974.341C1144.84 1020.73 1128.23 1072.28 1049.77 1072.57C1047.76 1072.57 1046.33 1074.57 1044.9 1076.29C1022.28 1106.64 994.785 1132.99 973.88 1153.89C920.616 1207.44 839.001 1343.47 764.831 1361.51C712.712 1374.4 647.993 1358.07 584.992 1367.81C551.773 1372.97 421.188 1389.29 392.552 1383.56C389.974 1382.99 388.256 1380.41 388.542 1377.83C388.829 1372.97 386.824 1369.53 392.552 1366.67C416.32 1360.94 437.511 1370.68 458.416 1366.38C489.153 1359.51 519.89 1352.73 550.627 1346.05C550.627 1346.05 550.723 1346.05 550.914 1346.05C633.101 1332.3 708.13 1339.17 783.731 1324.86C785.45 1324.57 793.468 1316.55 794.614 1315.12C818.669 1291.92 830.696 1278.46 847.305 1261C871.074 1236.08 896.275 1212.03 917.752 1185.11C946.676 1149.31 968.153 1128.69 1016.55 1075.14C1019.7 1071.71 1021.42 1069.99 1021.13 1067.98C1021.13 1066.27 1021.13 1063.97 1017.69 1062.26C982.472 1044.21 1000.51 990.95 1011.39 960.308C1025.71 920.217 1117.92 849.197 1181.78 863.229C1267.98 882.703 1306.35 937.685 1330.12 1021.02C1335.85 1040.78 1347.02 1070.85 1341.29 1098.91C1341.1 1099.1 1341.01 1099.29 1341.01 1099.48C1336.42 1111.51 1334.42 1128.69 1329.84 1140.43C1329.84 1140.63 1329.84 1140.72 1329.84 1140.72C1323.82 1162.77 1321.25 1178.24 1305.5 1191.69C1303.49 1193.41 1302.92 1196.28 1304.35 1198.28C1335.85 1252.98 1349.31 1322.57 1351.89 1402.18C1353.61 1446.56 1349.88 1510.71 1357.61 1547.94C1357.9 1549.66 1359.33 1551.09 1360.76 1551.66C1426.63 1580.3 1593.87 1723.48 1613.34 1789.92C1613.53 1790.87 1613.53 1791.73 1613.34 1792.5C1608.76 1811.97 1602.17 1830.3 1595.59 1848.05C1595.4 1848.62 1595.01 1849.2 1594.44 1849.77C1549.77 1898.74 1458.99 1955.73 1360.76 1912.49C1304.92 1887.86 1267.41 1854.35 1229.61 1809.68C1203.83 1779.32 1178.63 1744.96 1153.72 1713.17C1112.2 1660.48 1078.98 1602.92 1039.75 1547.36C1022.56 1523.31 1006.53 1495.25 988.199 1471.19C988.199 1471 988.104 1470.81 987.913 1470.62C975.885 1458.21 963.953 1445.8 952.117 1433.39C948.966 1427.66 950.971 1426.23 954.121 1423.37C954.885 1422.79 955.744 1422.41 956.698 1422.22C977.317 1418.79 998.508 1448.85 1008.53 1462.03C1008.53 1462.22 1008.63 1462.31 1008.82 1462.31C1027.43 1481.21 1042.9 1508.13 1060.08 1529.61C1060.27 1529.8 1060.36 1529.9 1060.36 1529.9C1076.4 1554.81 1092.15 1572.57 1108.47 1597.48C1170.62 1685.68 1238.77 1803.67 1330.12 1861.51C1354.46 1876.98 1403.43 1909.05 1445.82 1900.46C1493.07 1891.01 1524.57 1870.1 1557.5 1849.77C1573.25 1839.75 1578.69 1819.13 1569.53 1802.52C1521.13 1716.32 1422.91 1613.8 1332.41 1587.46C1330.12 1586.88 1328.69 1584.88 1328.41 1582.59C1327.45 1539.44 1326.5 1496.2 1325.54 1452.86C1325.54 1450 1323.25 1447.71 1320.39 1447.71Z" fill="#EC92AD"/>
|
||||
<path d="M562.082 1052.23C664.889 1048.51 591.005 1225.2 519.986 1200.86C472.162 1184.25 501.945 1104.64 521.418 1078.87C531.441 1065.98 547.191 1060.82 562.082 1052.23Z" fill="#EC92AD"/>
|
||||
<path d="M407.443 1066.27C486.194 1071.14 421.188 1213.46 365.346 1194.56C292.895 1169.93 373.365 1082.59 407.443 1066.27Z" fill="#EC92AD"/>
|
||||
<path d="M656.011 1106.94C689.516 1129.85 715.575 1222.06 691.807 1266.44C675.484 1270.17 603.319 1289.93 588.714 1271.02C588.714 1268.92 588.714 1266.82 588.714 1264.72C644.842 1243.53 657.729 1182.25 656.011 1106.94Z" fill="#EC92AD"/>
|
||||
<path d="M491.921 1117.81C489.058 1157.33 478.748 1214.6 474.739 1239.8C471.303 1260.14 481.039 1286.77 473.021 1300.8C463.284 1306.91 453.452 1313.11 443.525 1319.41C407.442 1314.55 398.565 1267.01 398.279 1227.2C424.911 1215.46 484.762 1149.88 491.921 1117.81Z" fill="#EC92AD"/>
|
||||
<path d="M201.256 1280.47C223.307 1279.9 256.812 1294.79 265.403 1308.53C187.224 1307.96 170.901 1389.29 159.16 1456.87C157.633 1456.3 156.105 1455.82 154.578 1455.44C106.755 1371.25 146.846 1330.87 201.256 1280.47Z" fill="#EC92AD"/>
|
||||
<path d="M398.279 1311.69C410.307 1315.41 418.611 1316.55 429.78 1324.57C434.075 1327.72 432.644 1330.59 428.062 1335.74C406.584 1354.07 376.229 1365.81 351.315 1363.23C336.71 1361.8 315.805 1360.65 306.355 1366.95C304.064 1368.1 304.35 1370.1 304.637 1371.82C312.082 1413.06 373.938 1518.16 357.329 1551.38C352.46 1551.66 351.028 1551.66 346.446 1551.66C326.401 1522.17 257.672 1407.62 292.036 1352.35C309.505 1324 371.933 1331.73 398.279 1311.69Z" fill="#EC92AD"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2718_972" x1="36.3572" y1="2416.52" x2="36.3572" y2="806.412" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F1D0DB"/>
|
||||
<stop offset="1" stop-color="#F1D0DB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_2718_972" x1="36.3572" y1="2416.52" x2="36.3572" y2="806.412" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F1D0DB"/>
|
||||
<stop offset="1" stop-color="#F1D0DB"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_2718_972">
|
||||
<rect width="1500" height="1500" rx="250" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
import { OnboardingDialog, ToasterRoot } from '@proj-airi/stage-ui/components'
|
||||
import { useSharedAnalyticsStore } from '@proj-airi/stage-ui/stores/analytics'
|
||||
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
|
||||
import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/channel-server'
|
||||
import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/context-bridge'
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
import { useOnboardingStore } from '@proj-airi/stage-ui/stores/onboarding'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { useTheme } from '@proj-airi/ui'
|
||||
import { StageTransitionGroup } from '@proj-airi/ui-transitions'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RouterView } from 'vue-router'
|
||||
import { toast, Toaster } from 'vue-sonner'
|
||||
|
||||
import 'vue-sonner/style.css'
|
||||
|
||||
const contextBridgeStore = useContextBridgeStore()
|
||||
const i18n = useI18n()
|
||||
const displayModelsStore = useDisplayModelsStore()
|
||||
const settingsStore = useSettings()
|
||||
const settings = storeToRefs(settingsStore)
|
||||
const onboardingStore = useOnboardingStore()
|
||||
const serverChannelStore = useModsServerChannelStore()
|
||||
const { shouldShowSetup } = storeToRefs(onboardingStore)
|
||||
const { isDark } = useTheme()
|
||||
const cardStore = useAiriCardStore()
|
||||
const analyticsStore = useSharedAnalyticsStore()
|
||||
|
||||
const primaryColor = computed(() => {
|
||||
return isDark.value
|
||||
? `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 70%, oklch(50% 0 360))`
|
||||
: `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 90%, oklch(90% 0 360))`
|
||||
})
|
||||
|
||||
const secondaryColor = computed(() => {
|
||||
return isDark.value
|
||||
? `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${180})) 70%, oklch(50% 0 360))`
|
||||
: `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${180})) 90%, oklch(90% 0 360))`
|
||||
})
|
||||
|
||||
const tertiaryColor = computed(() => {
|
||||
return isDark.value
|
||||
? `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${60})) 70%, oklch(50% 0 360))`
|
||||
: `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${60})) 90%, oklch(90% 0 360))`
|
||||
})
|
||||
|
||||
const colors = computed(() => {
|
||||
return [primaryColor.value, secondaryColor.value, tertiaryColor.value, isDark.value ? '#121212' : '#FFFFFF']
|
||||
})
|
||||
|
||||
watch(settings.language, () => {
|
||||
i18n.locale.value = settings.language.value
|
||||
})
|
||||
|
||||
watch(settings.themeColorsHue, () => {
|
||||
document.documentElement.style.setProperty('--chromatic-hue', settings.themeColorsHue.value.toString())
|
||||
}, { immediate: true })
|
||||
|
||||
watch(settings.themeColorsHueDynamic, () => {
|
||||
document.documentElement.classList.toggle('dynamic-hue', settings.themeColorsHueDynamic.value)
|
||||
}, { immediate: true })
|
||||
|
||||
// Initialize first-time setup check when app mounts
|
||||
onMounted(async () => {
|
||||
analyticsStore.initialize()
|
||||
cardStore.initialize()
|
||||
|
||||
onboardingStore.initializeSetupCheck()
|
||||
|
||||
await serverChannelStore.initialize({ possibleEvents: ['ui:configure'] }).catch((err) => { console.error('Failed to initialize Mods Server Channel in App.vue:', err) })
|
||||
await contextBridgeStore.initialize()
|
||||
|
||||
await displayModelsStore.loadDisplayModelsFromIndexedDB()
|
||||
await settingsStore.initializeStageModel()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
contextBridgeStore.dispose()
|
||||
})
|
||||
|
||||
// Handle first-time setup events
|
||||
function handleSetupConfigured() {
|
||||
onboardingStore.markSetupCompleted()
|
||||
}
|
||||
|
||||
function handleSetupSkipped() {
|
||||
onboardingStore.markSetupSkipped()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<StageTransitionGroup
|
||||
:primary-color="primaryColor"
|
||||
:secondary-color="secondaryColor"
|
||||
:tertiary-color="tertiaryColor"
|
||||
:colors="colors"
|
||||
:z-index="100"
|
||||
:disable-transitions="settings.disableTransitions.value"
|
||||
:use-page-specific-transitions="settings.usePageSpecificTransitions.value"
|
||||
>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<KeepAlive :include="['IndexScenePage', 'StageScenePage']">
|
||||
<component :is="Component" />
|
||||
</KeepAlive>
|
||||
</RouterView>
|
||||
</StageTransitionGroup>
|
||||
|
||||
<ToasterRoot @close="id => toast.dismiss(id)">
|
||||
<Toaster />
|
||||
</ToasterRoot>
|
||||
|
||||
<!-- First Time Setup Dialog -->
|
||||
<OnboardingDialog
|
||||
v-model="shouldShowSetup"
|
||||
@configured="handleSetupConfigured"
|
||||
@skipped="handleSetupSkipped"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* We need this to properly animate the CSS variable */
|
||||
@property --chromatic-hue {
|
||||
syntax: '<number>';
|
||||
initial-value: 0;
|
||||
inherits: true;
|
||||
}
|
||||
|
||||
@keyframes hue-anim {
|
||||
from {
|
||||
--chromatic-hue: 0;
|
||||
}
|
||||
to {
|
||||
--chromatic-hue: 360;
|
||||
}
|
||||
}
|
||||
|
||||
.dynamic-hue {
|
||||
animation: hue-anim 10s linear infinite;
|
||||
}
|
||||
</style>
|
||||
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 209 KiB |
@@ -0,0 +1,61 @@
|
||||
<svg width="1500" height="1500" viewBox="0 0 1500 1500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2718_906)">
|
||||
<path d="M442.5 953.5L669 697L1080 927L985.5 1134.5L669 1104.5L442.5 953.5Z" fill="white"/>
|
||||
<path d="M985.5 727L1034.5 602.5L1121.5 629L1166.5 844L985.5 727Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1540.03 1958.88C1518.27 1990.95 1454.98 2031.91 1419.76 2052.52C1403.15 2062.26 1385.11 2059.4 1360.48 2063.41C1254.24 2080.59 1148.57 2057.97 1062.08 2032.19C1041.18 2025.89 989.344 1986.37 967.008 1994.68C934.934 2009 939.516 2047.66 923.193 2077.44C912.311 2100.35 883.674 2183.68 874.797 2202.3C852.174 2249.83 823.823 2298.8 796.618 2342.9C775.713 2376.98 722.162 2440.55 662.311 2407.05C634.247 2391.3 635.679 2369.53 631.383 2346.91C631.097 2346.05 631.097 2333.74 630.238 2333.74C627.088 2333.74 627.088 2333.74 623.365 2333.74C616.11 2342.52 608.76 2351.3 601.315 2360.08C559.791 2398.17 497.363 2389.01 482.758 2332.02C473.021 2294.51 491.062 2256.71 501.372 2232.08C520.654 2172.7 539.936 2113.33 559.218 2053.96C569.528 2004.7 546.045 1964.61 532.586 1932.25C507.385 1870.39 500.226 1796.8 474.739 1733.8C464.144 1707.16 443.811 1693.42 435.793 1674.52C426.343 1651.89 405.438 1559.11 388.829 1551.09C335.564 1527.04 321.246 1607.22 290.604 1618.1C228.176 1640.44 200.971 1552.52 196.961 1507.28C145.129 1490.1 106.183 1412.2 131.097 1340.32C153.147 1277.61 199.539 1232.36 287.454 1234.08C295.186 1224.35 307.214 1219.48 315.518 1210.6L332.7 1141.87C341.673 1125.26 350.551 1108.56 359.333 1091.76C371.647 1073.72 393.983 1050.81 412.311 1038.78C422.811 1033.62 433.216 1028.37 443.525 1023.03C437.798 1021.69 432.643 1020.07 428.061 1018.16C398.852 1005.27 393.983 980.359 373.365 956.017C371.36 961.172 348.451 999.545 335.85 1010.71C331.65 1008.04 327.45 1005.37 323.25 1002.7C315.805 990.954 315.518 909.053 309.218 884.139C259.104 683.108 276.572 507.563 421.761 407.621C433.216 399.889 441.234 376.407 445.243 370.106C472.735 350.824 500.322 331.542 528.004 312.26C557.786 297.655 598.737 295.364 641.979 289.064C658.588 288.491 675.293 287.918 692.093 287.346C818.955 286.2 964.717 357.22 1037.17 415.353C1053.78 436.735 1070.48 458.117 1087.28 479.499C1121.36 525.032 1159.45 565.983 1190.38 615.525C1219.59 662.489 1281.73 738.377 1280.87 801.378C1249.56 767.396 1218.34 733.509 1187.23 699.717C1195.82 736.086 1215.29 743.531 1229.32 769.305C1233.62 777.323 1239.34 786.773 1245.07 791.928C1252.52 796.701 1260.06 801.569 1267.7 806.533C1285.64 817.797 1303.59 829.06 1321.53 840.324C1379.38 882.993 1437.23 942.844 1477.6 1002.7C1548.05 1107.22 1598.74 1224.35 1630.81 1368.39C1677.78 1579.44 1628.23 1830.3 1540.03 1958.88ZM1048.05 716.899C1071.82 724.345 1106.18 762.146 1133.96 754.414C1133.96 754.414 1103.89 725.204 1090.43 710.599C1080.41 697.14 1079.55 686.258 1067.24 666.212L1059.79 657.048C1055.5 674.803 1040.6 702.294 1048.05 716.899ZM1005.95 984.081C1005.09 982.65 995.358 998.4 994.786 997.541C987.34 987.518 983.044 988.091 971.303 979.786C961.853 968.331 957.558 943.99 948.107 932.535C926.916 935.399 896.275 1010.14 879.379 1027.9C878.424 1027.32 877.374 1026.75 876.229 1026.18C863.056 978.641 766.549 821.138 732.471 801.378C706.507 796.605 680.543 791.928 654.579 787.346C629.665 814.551 618.21 854.643 592.151 880.989C565.518 907.621 530.009 925.09 504.522 952.867V965.181C513.399 978.354 527.718 981.218 542.036 988.663C561.891 998.018 581.651 1007.37 601.315 1016.73C607.328 1018.16 613.628 1018.45 619.642 1017.87C630.524 1017.01 641.12 1014.15 648.279 1012.15C680.066 1003.27 717.58 1011.57 746.79 1018.45C767.122 1023.31 778.577 1037.35 793.468 1046.51C821.532 1063.98 850.742 1069.71 876.229 1088.61C879.474 1092.81 882.624 1097.01 885.679 1101.21C913.743 1112.09 1004.52 1012.43 1005.95 984.081Z" fill="#F1D0DB"/>
|
||||
<path d="M534.877 418.215L635.965 159.91C635.965 159.91 712.712 199.715 801.773 395.305L534.877 418.215Z" fill="#F1D0DB"/>
|
||||
<path d="M197.82 483.511C197.82 483.511 265.117 459.17 335.277 475.493C405.724 491.816 279.722 635.86 279.722 635.86C279.722 635.86 184.934 508.139 197.82 483.511Z" fill="#F1D0DB"/>
|
||||
<path d="M531.154 378.41C531.154 378.41 528.863 423.084 540.89 424.229L636.251 160.197C636.251 159.911 635.965 159.911 635.965 160.197C513.685 382.706 535.736 351.492 531.154 378.41Z" fill="#EC92AD"/>
|
||||
<path d="M789.745 380.417C774.854 371.253 756.813 319.42 755.667 315.411C746.503 282.192 724.453 255.846 678.92 206.591C655.724 181.39 635.106 161.058 635.965 159.913C636.824 159.053 655.438 173.945 666.034 182.822C690.948 203.441 713.857 226.064 742.208 261.574C794.327 326.293 806.354 373.83 816.377 389.294C816.664 389.867 818.096 392.158 817.236 393.017C816.091 394.449 809.505 391.012 801.773 387.003C796.332 384.139 791.177 381.276 789.745 380.417Z" fill="#EC92AD"/>
|
||||
<path d="M566.95 414.495C564.373 412.777 576.973 386.717 596.446 356.935C612.483 332.307 614.201 334.311 629.092 309.97C637.397 296.797 644.843 283.624 646.561 284.483C647.706 284.77 646.274 289.065 645.129 295.079C639.115 324.289 646.847 338.893 643.983 359.798C643.983 360.944 643.411 364.38 644.556 364.667C645.702 365.239 648.852 359.798 652.002 355.216C655.152 350.921 660.306 346.339 670.329 337.461C690.089 319.707 692.093 320.279 692.953 320.852C694.671 321.998 692.666 325.72 692.953 339.466C692.953 340.612 692.953 351.207 697.534 359.798C698.107 360.657 698.966 362.089 700.398 362.948C702.403 364.094 705.266 364.38 712.139 361.23C722.448 356.362 723.88 351.494 727.317 352.353C730.181 353.212 729.608 356.935 734.762 366.385C738.485 373.544 751.085 375.549 749.94 377.84C748.508 380.703 731.612 381.562 729.894 381.849C670.329 391.585 601.887 408.767 582.987 410.486C581.555 410.772 577.832 411.058 573.25 412.49C568.668 413.922 567.523 415.068 566.95 414.495Z" fill="#EC92AD"/>
|
||||
<path d="M280.581 628.703C283.158 630.994 281.44 639.585 278.576 640.444C276.572 641.017 272.849 638.726 260.821 618.68C248.221 597.775 242.78 590.902 231.039 568.852C220.443 548.233 214.43 534.201 210.707 529.047C210.707 529.047 200.684 512.437 195.243 494.682C194.384 491.819 194.098 488.669 194.098 488.669C194.098 486.378 193.811 483.8 194.384 482.941C196.388 480.364 233.616 498.691 267.981 522.746C277.431 529.619 298.909 545.37 305.495 546.515C305.781 546.801 306.641 546.802 307.213 547.661C309.504 550.524 300.34 556.824 297.19 566.275C289.745 586.893 289.745 605.221 287.167 610.375C283.731 616.962 281.44 621.257 280.581 621.257C279.722 621.257 279.435 606.653 279.435 606.653C279.435 606.653 268.84 608.944 267.408 606.939C263.971 601.498 285.736 584.889 282.586 579.448C280.295 575.438 265.976 580.879 264.831 578.016C263.112 574.579 281.726 568.279 281.726 559.115C281.726 546.515 245.644 540.788 227.316 521.315C226.743 520.456 225.598 519.31 224.739 519.596C221.303 521.315 244.785 573.72 255.667 595.484C267.408 618.394 269.126 614.957 273.994 622.689C274.567 623.548 275.713 625.553 278.004 627.271C279.435 628.13 280.008 628.13 280.581 628.703Z" fill="#EC92AD"/>
|
||||
<path d="M1540.03 1958.88C1518.27 1990.95 1454.98 2031.9 1419.76 2052.52C1403.15 2062.26 1385.11 2059.39 1360.48 2063.4C1254.24 2080.58 1148.57 2057.96 1062.08 2032.19C1041.18 2025.89 989.344 1986.37 967.008 1994.67C934.934 2008.99 939.516 2047.65 923.193 2077.43C912.311 2100.34 883.674 2183.68 874.797 2202.29C852.174 2249.83 823.823 2298.8 796.618 2342.9C775.713 2376.98 722.162 2440.55 662.311 2407.05C634.247 2391.3 635.679 2369.53 631.383 2346.91C631.097 2346.05 631.097 2333.74 630.238 2333.74C627.088 2333.74 627.088 2333.74 623.365 2333.74C616.11 2342.52 608.76 2351.3 601.315 2360.08C559.791 2398.17 497.363 2389 482.758 2332.02C473.021 2294.5 491.062 2256.7 501.372 2232.07C520.654 2172.7 539.936 2113.33 559.218 2053.95C569.528 2004.7 546.045 1964.61 532.586 1932.25C507.385 1870.39 500.226 1796.79 474.739 1733.79C464.144 1707.16 443.811 1693.41 435.793 1674.51C426.343 1651.89 405.438 1559.11 388.829 1551.09C335.564 1527.03 321.246 1607.22 290.604 1618.1C228.176 1640.44 200.971 1552.52 196.961 1507.27C145.129 1490.09 106.183 1412.2 131.097 1340.32C153.147 1277.61 199.539 1232.36 287.454 1234.08C295.186 1224.34 307.214 1219.47 315.518 1210.6L332.7 1141.87C341.673 1125.26 350.551 1108.55 359.333 1091.75C371.647 1073.71 393.983 1050.8 412.311 1038.77C422.811 1033.62 433.216 1028.37 443.525 1023.02C437.798 1021.69 432.643 1020.07 428.061 1018.16C438.943 1015.01 448.68 1014.15 456.698 1014.15C497.076 1013.86 513.399 1036.77 557.786 1035.34C577.832 1034.48 577.832 1029.61 619.642 1017.87C682.643 1000.11 714.144 991.237 746.217 998.969C783.445 1007.85 777.431 1027.03 841.865 1056.24C875.656 1071.42 899.711 1082.02 925.484 1073.14C950.685 1064.26 968.726 1040.21 975.599 1031.04C985.908 1017.01 991.349 1004.7 993.067 1000.97C993.067 1000.97 996.217 994.101 1003.66 980.642C1015.4 959.737 1037.45 923.941 1074.68 874.399C1114.2 822.566 1204.41 809.107 1267.7 806.529C1285.64 817.793 1303.59 829.057 1321.53 840.321C1379.38 882.99 1437.23 942.841 1477.6 1002.69C1548.05 1107.22 1598.74 1224.34 1630.81 1368.39C1677.78 1579.44 1628.23 1830.3 1540.03 1958.88Z" fill="url(#paint0_linear_2718_906)"/>
|
||||
<path d="M402.861 438.549C404.579 468.618 379.665 549.946 374.797 567.988C373.651 568.274 372.219 568.56 370.501 568.847C366.778 569.133 363.056 568.274 359.906 565.983L359.333 565.697C352.746 518.159 378.52 470.336 402.861 438.549Z" fill="#EC92AD"/>
|
||||
<path d="M339.574 488.086C332.987 507.559 322.392 530.469 315.805 549.942C315.423 550.514 315.232 551.183 315.232 551.946C304.064 604.638 301.2 731.213 312.369 785.337C325.351 839.938 338.333 894.539 351.315 949.14C358.761 990.664 306.355 1080.3 284.018 1085.17C284.591 1084.31 287.455 1071.42 290.032 1060.82C291.368 1054.91 292.227 1048.99 292.609 1043.07C295.091 1005.46 297.478 967.849 299.769 930.24C300.914 915.921 299.482 901.603 296.046 887.571C287.836 854.543 279.723 821.515 271.704 788.487C257.672 716.036 278.864 597.479 299.769 546.219C308.073 525.887 338.715 488.086 339.574 488.086Z" fill="#EC92AD"/>
|
||||
<path d="M576.114 529.043C626.228 550.521 648.565 618.104 679.207 660.2C716.148 710.887 757.385 762.434 788.6 817.989C792.895 830.589 828.119 899.318 841.864 921.082C875.083 977.783 907.443 1088.89 907.443 1174.23C906.297 1173.66 816.377 968.047 815.232 968.047C820.386 1070.57 800.054 1107.22 800.054 1106.08C784.59 1036.49 782.013 973.774 760.535 921.082C705.266 785.63 650.57 646.741 576.114 529.043Z" fill="#EC92AD"/>
|
||||
<path d="M365.346 604.062C379.665 618.667 383.674 682.241 390.261 707.155C400.761 742.092 411.165 776.934 421.475 811.68C441.807 858.358 476.744 885.85 474.739 945.987C473.212 945.987 471.589 945.987 469.871 945.987C386.824 913.914 351.314 728.06 365.346 604.062Z" fill="#EC92AD"/>
|
||||
<path d="M1042.9 808.247C1042.32 854.925 992.495 949.427 971.876 983.791C968.44 984.364 934.362 971.763 931.212 960.309C928.348 949.14 987.054 802.233 994.786 795.646C1009.1 800.515 1025.71 807.101 1042.9 808.247Z" fill="#EC92AD"/>
|
||||
<path d="M1320.39 1447.71C1244.21 1448.28 1146.85 1396.45 1093.01 1365.23C1091.01 1364.09 1089.86 1361.51 1090.72 1359.22C1091.29 1356.93 1092.15 1354.64 1092.72 1352.63C1093.58 1350.34 1095.59 1349.2 1097.88 1349.2C1132.82 1350.63 1145.42 1371.25 1169.18 1381.84C1208.42 1399.6 1265.12 1417.64 1316.95 1419.36C1319.81 1419.36 1322.39 1417.07 1322.39 1413.92C1322.96 1338.32 1314.66 1270.73 1293.75 1219.47C1293.56 1219.28 1293.47 1219.09 1293.47 1218.9C1292.04 1212.6 1288.31 1206.59 1286.6 1200.29C1286.6 1199.71 1286.02 1198.85 1285.45 1198.28C1281.44 1193.99 1274.57 1194.27 1270.85 1188.26C1267.12 1182.24 1294.04 1155.33 1297.76 1137.57C1311.51 1071.42 1246.22 968.899 1208.42 935.394C1203.83 936.826 1202.69 937.399 1198.11 938.831C1195.24 956.299 1185.51 962.027 1177.2 974.341C1144.84 1020.73 1128.23 1072.28 1049.77 1072.57C1047.76 1072.57 1046.33 1074.57 1044.9 1076.29C1022.28 1106.64 994.785 1132.99 973.88 1153.89C920.616 1207.44 839.001 1343.47 764.831 1361.51C712.712 1374.4 647.993 1358.07 584.992 1367.81C551.773 1372.97 421.188 1389.29 392.552 1383.56C389.974 1382.99 388.256 1380.41 388.542 1377.83C388.829 1372.97 386.824 1369.53 392.552 1366.67C416.32 1360.94 437.511 1370.68 458.416 1366.38C489.153 1359.51 519.89 1352.73 550.627 1346.05C550.627 1346.05 550.723 1346.05 550.914 1346.05C633.101 1332.3 708.13 1339.17 783.731 1324.86C785.45 1324.57 793.468 1316.55 794.614 1315.12C818.669 1291.92 830.696 1278.46 847.305 1261C871.074 1236.08 896.275 1212.03 917.752 1185.11C946.676 1149.31 968.153 1128.69 1016.55 1075.14C1019.7 1071.71 1021.42 1069.99 1021.13 1067.98C1021.13 1066.27 1021.13 1063.97 1017.69 1062.26C982.472 1044.21 1000.51 990.95 1011.39 960.308C1025.71 920.217 1117.92 849.197 1181.78 863.229C1267.98 882.703 1306.35 937.685 1330.12 1021.02C1335.85 1040.78 1347.02 1070.85 1341.29 1098.91C1341.1 1099.1 1341.01 1099.29 1341.01 1099.48C1336.42 1111.51 1334.42 1128.69 1329.84 1140.43C1329.84 1140.63 1329.84 1140.72 1329.84 1140.72C1323.82 1162.77 1321.25 1178.24 1305.5 1191.69C1303.49 1193.41 1302.92 1196.28 1304.35 1198.28C1335.85 1252.98 1349.31 1322.57 1351.89 1402.18C1353.61 1446.56 1349.88 1510.71 1357.61 1547.94C1357.9 1549.66 1359.33 1551.09 1360.76 1551.66C1426.63 1580.3 1593.87 1723.48 1613.34 1789.92C1613.53 1790.87 1613.53 1791.73 1613.34 1792.5C1608.76 1811.97 1602.17 1830.3 1595.59 1848.05C1595.4 1848.62 1595.01 1849.2 1594.44 1849.77C1549.77 1898.74 1458.99 1955.73 1360.76 1912.49C1304.92 1887.86 1267.41 1854.35 1229.61 1809.68C1203.83 1779.32 1178.63 1744.96 1153.72 1713.17C1112.2 1660.48 1078.98 1602.92 1039.75 1547.36C1022.56 1523.31 1006.53 1495.25 988.199 1471.19C988.199 1471 988.104 1470.81 987.913 1470.62C975.885 1458.21 963.953 1445.8 952.117 1433.39C948.966 1427.66 950.971 1426.23 954.121 1423.37C954.885 1422.79 955.744 1422.41 956.698 1422.22C977.317 1418.79 998.508 1448.85 1008.53 1462.03C1008.53 1462.22 1008.63 1462.31 1008.82 1462.31C1027.43 1481.21 1042.9 1508.13 1060.08 1529.61C1060.27 1529.8 1060.36 1529.9 1060.36 1529.9C1076.4 1554.81 1092.15 1572.57 1108.47 1597.48C1170.62 1685.68 1238.77 1803.67 1330.12 1861.51C1354.46 1876.98 1403.43 1909.05 1445.82 1900.46C1493.07 1891.01 1524.57 1870.1 1557.5 1849.77C1573.25 1839.75 1578.69 1819.13 1569.53 1802.52C1521.13 1716.32 1422.91 1613.8 1332.41 1587.46C1330.12 1586.88 1328.69 1584.88 1328.41 1582.59C1327.45 1539.44 1326.5 1496.2 1325.54 1452.86C1325.54 1450 1323.25 1447.71 1320.39 1447.71Z" fill="#EC92AD"/>
|
||||
<path d="M562.082 1052.23C664.889 1048.51 591.005 1225.2 519.986 1200.86C472.162 1184.25 501.945 1104.64 521.418 1078.87C531.441 1065.98 547.191 1060.82 562.082 1052.23Z" fill="#EC92AD"/>
|
||||
<path d="M407.443 1066.27C486.194 1071.14 421.188 1213.46 365.346 1194.56C292.895 1169.93 373.365 1082.59 407.443 1066.27Z" fill="#EC92AD"/>
|
||||
<path d="M656.011 1106.94C689.516 1129.85 715.575 1222.06 691.807 1266.44C675.484 1270.17 603.319 1289.93 588.714 1271.02C588.714 1268.92 588.714 1266.82 588.714 1264.72C644.842 1243.53 657.729 1182.25 656.011 1106.94Z" fill="#EC92AD"/>
|
||||
<path d="M491.921 1117.81C489.058 1157.33 478.748 1214.6 474.739 1239.8C471.303 1260.14 481.039 1286.77 473.021 1300.8C463.284 1306.91 453.452 1313.11 443.525 1319.41C407.442 1314.55 398.565 1267.01 398.279 1227.2C424.911 1215.46 484.762 1149.88 491.921 1117.81Z" fill="#EC92AD"/>
|
||||
<path d="M201.256 1280.47C223.307 1279.9 256.812 1294.79 265.403 1308.53C187.224 1307.96 170.901 1389.29 159.16 1456.87C157.633 1456.3 156.105 1455.82 154.578 1455.44C106.755 1371.25 146.846 1330.87 201.256 1280.47Z" fill="#EC92AD"/>
|
||||
<path d="M398.279 1311.69C410.307 1315.41 418.611 1316.55 429.78 1324.57C434.075 1327.72 432.644 1330.59 428.062 1335.74C406.584 1354.07 376.229 1365.81 351.315 1363.23C336.71 1361.8 315.805 1360.65 306.355 1366.95C304.064 1368.1 304.35 1370.1 304.637 1371.82C312.082 1413.06 373.938 1518.16 357.329 1551.38C352.46 1551.66 351.028 1551.66 346.446 1551.66C326.401 1522.17 257.672 1407.62 292.036 1352.35C309.505 1324 371.933 1331.73 398.279 1311.69Z" fill="#EC92AD"/>
|
||||
<path d="M442.5 953.5L669 697L1080 927L985.5 1134.5L669 1104.5L442.5 953.5Z" fill="#FFEBEB"/>
|
||||
<path d="M985.5 727L1034.5 602.5L1121.5 629L1166.5 844L985.5 727Z" fill="#FFEBEB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1540.03 1958.88C1518.27 1990.95 1454.98 2031.91 1419.76 2052.52C1403.15 2062.26 1385.11 2059.4 1360.48 2063.41C1254.24 2080.59 1148.57 2057.97 1062.08 2032.19C1041.18 2025.89 989.344 1986.37 967.008 1994.68C934.934 2009 939.516 2047.66 923.193 2077.44C912.311 2100.35 883.674 2183.68 874.797 2202.3C852.174 2249.83 823.823 2298.8 796.618 2342.9C775.713 2376.98 722.162 2440.55 662.311 2407.05C634.247 2391.3 635.679 2369.53 631.383 2346.91C631.097 2346.05 631.097 2333.74 630.238 2333.74C627.088 2333.74 627.088 2333.74 623.365 2333.74C616.11 2342.52 608.76 2351.3 601.315 2360.08C559.791 2398.17 497.363 2389.01 482.758 2332.02C473.021 2294.51 491.062 2256.71 501.372 2232.08C520.654 2172.7 539.936 2113.33 559.218 2053.96C569.528 2004.7 546.045 1964.61 532.586 1932.25C507.385 1870.39 500.226 1796.8 474.739 1733.8C464.144 1707.16 443.811 1693.42 435.793 1674.52C426.343 1651.89 405.438 1559.11 388.829 1551.09C335.564 1527.04 321.246 1607.22 290.604 1618.1C228.176 1640.44 200.971 1552.52 196.961 1507.28C145.129 1490.1 106.183 1412.2 131.097 1340.32C153.147 1277.61 199.539 1232.36 287.454 1234.08C295.186 1224.35 307.214 1219.48 315.518 1210.6L332.7 1141.87C341.673 1125.26 350.551 1108.56 359.333 1091.76C371.647 1073.72 393.983 1050.81 412.311 1038.78C422.811 1033.62 433.216 1028.37 443.525 1023.03C437.798 1021.69 432.643 1020.07 428.061 1018.16C398.852 1005.27 393.983 980.359 373.365 956.017C371.36 961.172 348.451 999.545 335.85 1010.71C331.65 1008.04 327.45 1005.37 323.25 1002.7C315.805 990.954 315.518 909.053 309.218 884.139C259.104 683.108 276.572 507.563 421.761 407.621C433.216 399.889 441.234 376.407 445.243 370.106C472.735 350.824 500.322 331.542 528.004 312.26C557.786 297.655 598.737 295.364 641.979 289.064C658.588 288.491 675.293 287.918 692.093 287.346C818.955 286.2 964.717 357.22 1037.17 415.353C1053.78 436.735 1070.48 458.117 1087.28 479.499C1121.36 525.032 1159.45 565.983 1190.38 615.525C1219.59 662.489 1281.73 738.377 1280.87 801.378C1249.56 767.396 1218.34 733.509 1187.23 699.717C1195.82 736.086 1215.29 743.531 1229.32 769.305C1233.62 777.323 1239.34 786.773 1245.07 791.928C1252.52 796.701 1260.06 801.569 1267.7 806.533C1285.64 817.797 1303.59 829.06 1321.53 840.324C1379.38 882.993 1437.23 942.844 1477.6 1002.7C1548.05 1107.22 1598.74 1224.35 1630.81 1368.39C1677.78 1579.44 1628.23 1830.3 1540.03 1958.88ZM1048.05 716.899C1071.82 724.345 1106.18 762.146 1133.96 754.414C1133.96 754.414 1103.89 725.204 1090.43 710.599C1080.41 697.14 1079.55 686.258 1067.24 666.212L1059.79 657.048C1055.5 674.803 1040.6 702.294 1048.05 716.899ZM1005.95 984.081C1005.09 982.65 995.358 998.4 994.786 997.541C987.34 987.518 983.044 988.091 971.303 979.786C961.853 968.331 957.558 943.99 948.107 932.535C926.916 935.399 896.275 1010.14 879.379 1027.9C878.424 1027.32 877.374 1026.75 876.229 1026.18C863.056 978.641 766.549 821.138 732.471 801.378C706.507 796.605 680.543 791.928 654.579 787.346C629.665 814.551 618.21 854.643 592.151 880.989C565.518 907.621 530.009 925.09 504.522 952.867V965.181C513.399 978.354 527.718 981.218 542.036 988.663C561.891 998.018 581.651 1007.37 601.315 1016.73C607.328 1018.16 613.628 1018.45 619.642 1017.87C630.524 1017.01 641.12 1014.15 648.279 1012.15C680.066 1003.27 717.58 1011.57 746.79 1018.45C767.122 1023.31 778.577 1037.35 793.468 1046.51C821.532 1063.98 850.742 1069.71 876.229 1088.61C879.474 1092.81 882.624 1097.01 885.679 1101.21C913.743 1112.09 1004.52 1012.43 1005.95 984.081Z" fill="#F1D0DB"/>
|
||||
<path d="M534.877 418.215L635.965 159.91C635.965 159.91 712.712 199.715 801.773 395.305L534.877 418.215Z" fill="#F1D0DB"/>
|
||||
<path d="M197.82 483.511C197.82 483.511 265.117 459.17 335.277 475.493C405.724 491.816 279.722 635.86 279.722 635.86C279.722 635.86 184.934 508.139 197.82 483.511Z" fill="#F1D0DB"/>
|
||||
<path d="M531.154 378.41C531.154 378.41 528.863 423.084 540.89 424.229L636.251 160.197C636.251 159.911 635.965 159.911 635.965 160.197C513.685 382.706 535.736 351.492 531.154 378.41Z" fill="#EC92AD"/>
|
||||
<path d="M789.745 380.417C774.854 371.253 756.813 319.42 755.667 315.411C746.503 282.192 724.453 255.846 678.92 206.591C655.724 181.39 635.106 161.058 635.965 159.913C636.824 159.053 655.438 173.945 666.034 182.822C690.948 203.441 713.857 226.064 742.208 261.574C794.327 326.293 806.354 373.83 816.377 389.294C816.664 389.867 818.096 392.158 817.236 393.017C816.091 394.449 809.505 391.012 801.773 387.003C796.332 384.139 791.177 381.276 789.745 380.417Z" fill="#EC92AD"/>
|
||||
<path d="M566.95 414.495C564.373 412.777 576.973 386.717 596.446 356.935C612.483 332.307 614.201 334.311 629.092 309.97C637.397 296.797 644.843 283.624 646.561 284.483C647.706 284.77 646.274 289.065 645.129 295.079C639.115 324.289 646.847 338.893 643.983 359.798C643.983 360.944 643.411 364.38 644.556 364.667C645.702 365.239 648.852 359.798 652.002 355.216C655.152 350.921 660.306 346.339 670.329 337.461C690.089 319.707 692.093 320.279 692.953 320.852C694.671 321.998 692.666 325.72 692.953 339.466C692.953 340.612 692.953 351.207 697.534 359.798C698.107 360.657 698.966 362.089 700.398 362.948C702.403 364.094 705.266 364.38 712.139 361.23C722.448 356.362 723.88 351.494 727.317 352.353C730.181 353.212 729.608 356.935 734.762 366.385C738.485 373.544 751.085 375.549 749.94 377.84C748.508 380.703 731.612 381.562 729.894 381.849C670.329 391.585 601.887 408.767 582.987 410.486C581.555 410.772 577.832 411.058 573.25 412.49C568.668 413.922 567.523 415.068 566.95 414.495Z" fill="#EC92AD"/>
|
||||
<path d="M280.581 628.703C283.158 630.994 281.44 639.585 278.576 640.444C276.572 641.017 272.849 638.726 260.821 618.68C248.221 597.775 242.78 590.902 231.039 568.852C220.443 548.233 214.43 534.201 210.707 529.047C210.707 529.047 200.684 512.437 195.243 494.682C194.384 491.819 194.098 488.669 194.098 488.669C194.098 486.378 193.811 483.8 194.384 482.941C196.388 480.364 233.616 498.691 267.981 522.746C277.431 529.619 298.909 545.37 305.495 546.515C305.781 546.801 306.641 546.802 307.213 547.661C309.504 550.524 300.34 556.824 297.19 566.275C289.745 586.893 289.745 605.221 287.167 610.375C283.731 616.962 281.44 621.257 280.581 621.257C279.722 621.257 279.435 606.653 279.435 606.653C279.435 606.653 268.84 608.944 267.408 606.939C263.971 601.498 285.736 584.889 282.586 579.448C280.295 575.438 265.976 580.879 264.831 578.016C263.112 574.579 281.726 568.279 281.726 559.115C281.726 546.515 245.644 540.788 227.316 521.315C226.743 520.456 225.598 519.31 224.739 519.596C221.303 521.315 244.785 573.72 255.667 595.484C267.408 618.394 269.126 614.957 273.994 622.689C274.567 623.548 275.713 625.553 278.004 627.271C279.435 628.13 280.008 628.13 280.581 628.703Z" fill="#EC92AD"/>
|
||||
<path d="M1540.03 1958.88C1518.27 1990.95 1454.98 2031.9 1419.76 2052.52C1403.15 2062.26 1385.11 2059.39 1360.48 2063.4C1254.24 2080.58 1148.57 2057.96 1062.08 2032.19C1041.18 2025.89 989.344 1986.37 967.008 1994.67C934.934 2008.99 939.516 2047.65 923.193 2077.43C912.311 2100.34 883.674 2183.68 874.797 2202.29C852.174 2249.83 823.823 2298.8 796.618 2342.9C775.713 2376.98 722.162 2440.55 662.311 2407.05C634.247 2391.3 635.679 2369.53 631.383 2346.91C631.097 2346.05 631.097 2333.74 630.238 2333.74C627.088 2333.74 627.088 2333.74 623.365 2333.74C616.11 2342.52 608.76 2351.3 601.315 2360.08C559.791 2398.17 497.363 2389 482.758 2332.02C473.021 2294.5 491.062 2256.7 501.372 2232.07C520.654 2172.7 539.936 2113.33 559.218 2053.95C569.528 2004.7 546.045 1964.61 532.586 1932.25C507.385 1870.39 500.226 1796.79 474.739 1733.79C464.144 1707.16 443.811 1693.41 435.793 1674.51C426.343 1651.89 405.438 1559.11 388.829 1551.09C335.564 1527.03 321.246 1607.22 290.604 1618.1C228.176 1640.44 200.971 1552.52 196.961 1507.27C145.129 1490.09 106.183 1412.2 131.097 1340.32C153.147 1277.61 199.539 1232.36 287.454 1234.08C295.186 1224.34 307.214 1219.47 315.518 1210.6L332.7 1141.87C341.673 1125.26 350.551 1108.55 359.333 1091.75C371.647 1073.71 393.983 1050.8 412.311 1038.77C422.811 1033.62 433.216 1028.37 443.525 1023.02C437.798 1021.69 432.643 1020.07 428.061 1018.16C438.943 1015.01 448.68 1014.15 456.698 1014.15C497.076 1013.86 513.399 1036.77 557.786 1035.34C577.832 1034.48 577.832 1029.61 619.642 1017.87C682.643 1000.11 714.144 991.237 746.217 998.969C783.445 1007.85 777.431 1027.03 841.865 1056.24C875.656 1071.42 899.711 1082.02 925.484 1073.14C950.685 1064.26 968.726 1040.21 975.599 1031.04C985.908 1017.01 991.349 1004.7 993.067 1000.97C993.067 1000.97 996.217 994.101 1003.66 980.642C1015.4 959.737 1037.45 923.941 1074.68 874.399C1114.2 822.566 1204.41 809.107 1267.7 806.529C1285.64 817.793 1303.59 829.057 1321.53 840.321C1379.38 882.99 1437.23 942.841 1477.6 1002.69C1548.05 1107.22 1598.74 1224.34 1630.81 1368.39C1677.78 1579.44 1628.23 1830.3 1540.03 1958.88Z" fill="url(#paint1_linear_2718_906)"/>
|
||||
<path d="M402.861 438.549C404.579 468.618 379.665 549.946 374.797 567.988C373.651 568.274 372.219 568.56 370.501 568.847C366.778 569.133 363.056 568.274 359.906 565.983L359.333 565.697C352.746 518.159 378.52 470.336 402.861 438.549Z" fill="#EC92AD"/>
|
||||
<path d="M339.574 488.086C332.987 507.559 322.392 530.469 315.805 549.942C315.423 550.514 315.232 551.183 315.232 551.946C304.064 604.638 301.2 731.213 312.369 785.337C325.351 839.938 338.333 894.539 351.315 949.14C358.761 990.664 306.355 1080.3 284.018 1085.17C284.591 1084.31 287.455 1071.42 290.032 1060.82C291.368 1054.91 292.227 1048.99 292.609 1043.07C295.091 1005.46 297.478 967.849 299.769 930.24C300.914 915.921 299.482 901.603 296.046 887.571C287.836 854.543 279.723 821.515 271.704 788.487C257.672 716.036 278.864 597.479 299.769 546.219C308.073 525.887 338.715 488.086 339.574 488.086Z" fill="#EC92AD"/>
|
||||
<path d="M576.114 529.043C626.228 550.521 648.565 618.104 679.207 660.2C716.148 710.887 757.385 762.434 788.6 817.989C792.895 830.589 828.119 899.318 841.864 921.082C875.083 977.783 907.443 1088.89 907.443 1174.23C906.297 1173.66 816.377 968.047 815.232 968.047C820.386 1070.57 800.054 1107.22 800.054 1106.08C784.59 1036.49 782.013 973.774 760.535 921.082C705.266 785.63 650.57 646.741 576.114 529.043Z" fill="#EC92AD"/>
|
||||
<path d="M365.346 604.062C379.665 618.667 383.674 682.241 390.261 707.155C400.761 742.092 411.165 776.934 421.475 811.68C441.807 858.358 476.744 885.85 474.739 945.987C473.212 945.987 471.589 945.987 469.871 945.987C386.824 913.914 351.314 728.06 365.346 604.062Z" fill="#EC92AD"/>
|
||||
<path d="M1042.9 808.247C1042.32 854.925 992.495 949.427 971.876 983.791C968.44 984.364 934.362 971.763 931.212 960.309C928.348 949.14 987.054 802.233 994.786 795.646C1009.1 800.515 1025.71 807.101 1042.9 808.247Z" fill="#EC92AD"/>
|
||||
<path d="M1320.39 1447.71C1244.21 1448.28 1146.85 1396.45 1093.01 1365.23C1091.01 1364.09 1089.86 1361.51 1090.72 1359.22C1091.29 1356.93 1092.15 1354.64 1092.72 1352.63C1093.58 1350.34 1095.59 1349.2 1097.88 1349.2C1132.82 1350.63 1145.42 1371.25 1169.18 1381.84C1208.42 1399.6 1265.12 1417.64 1316.95 1419.36C1319.81 1419.36 1322.39 1417.07 1322.39 1413.92C1322.96 1338.32 1314.66 1270.73 1293.75 1219.47C1293.56 1219.28 1293.47 1219.09 1293.47 1218.9C1292.04 1212.6 1288.31 1206.59 1286.6 1200.29C1286.6 1199.71 1286.02 1198.85 1285.45 1198.28C1281.44 1193.99 1274.57 1194.27 1270.85 1188.26C1267.12 1182.24 1294.04 1155.33 1297.76 1137.57C1311.51 1071.42 1246.22 968.899 1208.42 935.394C1203.83 936.826 1202.69 937.399 1198.11 938.831C1195.24 956.299 1185.51 962.027 1177.2 974.341C1144.84 1020.73 1128.23 1072.28 1049.77 1072.57C1047.76 1072.57 1046.33 1074.57 1044.9 1076.29C1022.28 1106.64 994.785 1132.99 973.88 1153.89C920.616 1207.44 839.001 1343.47 764.831 1361.51C712.712 1374.4 647.993 1358.07 584.992 1367.81C551.773 1372.97 421.188 1389.29 392.552 1383.56C389.974 1382.99 388.256 1380.41 388.542 1377.83C388.829 1372.97 386.824 1369.53 392.552 1366.67C416.32 1360.94 437.511 1370.68 458.416 1366.38C489.153 1359.51 519.89 1352.73 550.627 1346.05C550.627 1346.05 550.723 1346.05 550.914 1346.05C633.101 1332.3 708.13 1339.17 783.731 1324.86C785.45 1324.57 793.468 1316.55 794.614 1315.12C818.669 1291.92 830.696 1278.46 847.305 1261C871.074 1236.08 896.275 1212.03 917.752 1185.11C946.676 1149.31 968.153 1128.69 1016.55 1075.14C1019.7 1071.71 1021.42 1069.99 1021.13 1067.98C1021.13 1066.27 1021.13 1063.97 1017.69 1062.26C982.472 1044.21 1000.51 990.95 1011.39 960.308C1025.71 920.217 1117.92 849.197 1181.78 863.229C1267.98 882.703 1306.35 937.685 1330.12 1021.02C1335.85 1040.78 1347.02 1070.85 1341.29 1098.91C1341.1 1099.1 1341.01 1099.29 1341.01 1099.48C1336.42 1111.51 1334.42 1128.69 1329.84 1140.43C1329.84 1140.63 1329.84 1140.72 1329.84 1140.72C1323.82 1162.77 1321.25 1178.24 1305.5 1191.69C1303.49 1193.41 1302.92 1196.28 1304.35 1198.28C1335.85 1252.98 1349.31 1322.57 1351.89 1402.18C1353.61 1446.56 1349.88 1510.71 1357.61 1547.94C1357.9 1549.66 1359.33 1551.09 1360.76 1551.66C1426.63 1580.3 1593.87 1723.48 1613.34 1789.92C1613.53 1790.87 1613.53 1791.73 1613.34 1792.5C1608.76 1811.97 1602.17 1830.3 1595.59 1848.05C1595.4 1848.62 1595.01 1849.2 1594.44 1849.77C1549.77 1898.74 1458.99 1955.73 1360.76 1912.49C1304.92 1887.86 1267.41 1854.35 1229.61 1809.68C1203.83 1779.32 1178.63 1744.96 1153.72 1713.17C1112.2 1660.48 1078.98 1602.92 1039.75 1547.36C1022.56 1523.31 1006.53 1495.25 988.199 1471.19C988.199 1471 988.104 1470.81 987.913 1470.62C975.885 1458.21 963.953 1445.8 952.117 1433.39C948.966 1427.66 950.971 1426.23 954.121 1423.37C954.885 1422.79 955.744 1422.41 956.698 1422.22C977.317 1418.79 998.508 1448.85 1008.53 1462.03C1008.53 1462.22 1008.63 1462.31 1008.82 1462.31C1027.43 1481.21 1042.9 1508.13 1060.08 1529.61C1060.27 1529.8 1060.36 1529.9 1060.36 1529.9C1076.4 1554.81 1092.15 1572.57 1108.47 1597.48C1170.62 1685.68 1238.77 1803.67 1330.12 1861.51C1354.46 1876.98 1403.43 1909.05 1445.82 1900.46C1493.07 1891.01 1524.57 1870.1 1557.5 1849.77C1573.25 1839.75 1578.69 1819.13 1569.53 1802.52C1521.13 1716.32 1422.91 1613.8 1332.41 1587.46C1330.12 1586.88 1328.69 1584.88 1328.41 1582.59C1327.45 1539.44 1326.5 1496.2 1325.54 1452.86C1325.54 1450 1323.25 1447.71 1320.39 1447.71Z" fill="#EC92AD"/>
|
||||
<path d="M562.082 1052.23C664.889 1048.51 591.005 1225.2 519.986 1200.86C472.162 1184.25 501.945 1104.64 521.418 1078.87C531.441 1065.98 547.191 1060.82 562.082 1052.23Z" fill="#EC92AD"/>
|
||||
<path d="M407.443 1066.27C486.194 1071.14 421.188 1213.46 365.346 1194.56C292.895 1169.93 373.365 1082.59 407.443 1066.27Z" fill="#EC92AD"/>
|
||||
<path d="M656.011 1106.94C689.516 1129.85 715.575 1222.06 691.807 1266.44C675.484 1270.17 603.319 1289.93 588.714 1271.02C588.714 1268.92 588.714 1266.82 588.714 1264.72C644.842 1243.53 657.729 1182.25 656.011 1106.94Z" fill="#EC92AD"/>
|
||||
<path d="M491.921 1117.81C489.058 1157.33 478.748 1214.6 474.739 1239.8C471.303 1260.14 481.039 1286.77 473.021 1300.8C463.284 1306.91 453.452 1313.11 443.525 1319.41C407.442 1314.55 398.565 1267.01 398.279 1227.2C424.911 1215.46 484.762 1149.88 491.921 1117.81Z" fill="#EC92AD"/>
|
||||
<path d="M201.256 1280.47C223.307 1279.9 256.812 1294.79 265.403 1308.53C187.224 1307.96 170.901 1389.29 159.16 1456.87C157.633 1456.3 156.105 1455.82 154.578 1455.44C106.755 1371.25 146.846 1330.87 201.256 1280.47Z" fill="#EC92AD"/>
|
||||
<path d="M398.279 1311.69C410.307 1315.41 418.611 1316.55 429.78 1324.57C434.075 1327.72 432.644 1330.59 428.062 1335.74C406.584 1354.07 376.229 1365.81 351.315 1363.23C336.71 1361.8 315.805 1360.65 306.355 1366.95C304.064 1368.1 304.35 1370.1 304.637 1371.82C312.082 1413.06 373.938 1518.16 357.329 1551.38C352.46 1551.66 351.028 1551.66 346.446 1551.66C326.401 1522.17 257.672 1407.62 292.036 1352.35C309.505 1324 371.933 1331.73 398.279 1311.69Z" fill="#EC92AD"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2718_906" x1="36.3572" y1="2416.52" x2="36.3572" y2="806.412" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F1D0DB"/>
|
||||
<stop offset="1" stop-color="#F1D0DB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_2718_906" x1="36.3572" y1="2416.52" x2="36.3572" y2="806.412" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F1D0DB"/>
|
||||
<stop offset="1" stop-color="#F1D0DB"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_2718_906">
|
||||
<rect width="1500" height="1500" rx="375" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,40 @@
|
||||
<svg width="1500" height="1500" viewBox="0 0 1500 1500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_526_1314)">
|
||||
<path d="M693.023 1136.16L460.216 984.793L629.931 711.934L1043.81 948.943L1004.69 1172.01L693.023 1136.16Z" fill="#FFF4F4"/>
|
||||
<path d="M1019.84 783.634V629.611H1108.79L1176.93 844.048L1019.84 783.634Z" fill="#FFF4F4"/>
|
||||
<g clip-path="url(#clip1_526_1314)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1527.94 2023.17C1506.37 2056.62 1443.64 2099.34 1408.72 2120.84C1392.26 2131 1374.38 2128.01 1349.96 2132.19C1244.65 2150.11 1139.91 2126.52 1054.18 2099.64C1033.46 2093.06 982.082 2051.84 959.941 2060.51C928.149 2075.44 932.691 2115.76 916.511 2146.83C905.724 2170.72 877.338 2257.65 868.539 2277.06C846.114 2326.64 818.012 2377.72 791.045 2423.72C770.323 2459.27 717.242 2525.58 657.915 2490.63C630.097 2474.2 631.516 2451.5 627.258 2427.9C626.974 2427.01 626.974 2414.16 626.123 2414.16C623 2414.16 623 2414.16 619.31 2414.16C612.119 2423.32 604.833 2432.48 597.453 2441.64C556.293 2481.37 494.412 2471.81 479.935 2412.37C470.284 2373.24 488.167 2333.81 498.386 2308.13C517.499 2246.2 536.612 2184.27 555.725 2122.34C565.944 2070.96 542.668 2029.14 529.327 1995.39C504.347 1930.87 497.25 1854.11 471.987 1788.39C461.484 1760.61 441.33 1746.28 433.382 1726.56C424.014 1702.97 403.293 1606.19 386.829 1597.82C334.031 1572.73 319.838 1656.37 289.465 1667.72C227.584 1691.02 200.617 1599.32 196.643 1552.12C145.264 1534.2 106.66 1452.96 131.355 1377.98C153.213 1312.57 199.198 1265.37 286.343 1267.17C294.007 1257.01 305.929 1251.93 314.161 1242.67L331.193 1170.99C340.087 1153.66 348.886 1136.24 357.591 1118.71C369.797 1099.9 391.939 1076 410.106 1063.46C420.514 1058.08 430.827 1052.6 441.046 1047.03C435.369 1045.63 430.26 1043.94 425.718 1041.95C396.764 1028.51 391.938 1002.52 371.501 977.132C369.513 982.508 346.805 1022.53 334.315 1034.18C330.152 1031.4 325.988 1028.61 321.825 1025.82C314.445 1013.57 314.161 928.146 307.916 902.159C258.241 692.474 275.556 509.372 419.473 405.127C430.827 397.062 438.775 372.569 442.749 365.998C470 345.886 497.345 325.773 524.785 305.661C554.306 290.428 594.898 288.038 637.761 281.467C654.225 280.869 670.783 280.272 687.436 279.674C813.186 278.48 957.671 352.557 1029.49 413.192C1045.95 435.495 1062.51 457.797 1079.16 480.1C1112.94 527.593 1150.7 570.307 1181.35 621.981C1210.31 670.968 1271.9 750.122 1271.05 815.836C1240.02 780.39 1209.08 745.044 1178.23 709.798C1186.75 747.733 1206.05 755.499 1219.96 782.381C1224.21 790.745 1229.89 800.602 1235.57 805.979C1242.95 810.957 1250.42 816.035 1257.99 821.212C1275.78 832.961 1293.57 844.71 1311.36 856.458C1368.7 900.964 1426.04 963.392 1466.06 1025.82C1535.89 1134.84 1586.14 1257.01 1617.93 1407.26C1664.48 1627.4 1615.37 1889.05 1527.94 2023.17ZM1040.27 727.72C1063.83 735.486 1097.9 774.914 1125.43 766.849C1125.43 766.849 1095.63 736.382 1082.29 721.148C1072.35 707.11 1071.5 695.759 1059.29 674.85L1051.91 665.292C1047.65 683.812 1032.89 712.486 1040.27 727.72ZM998.546 1006.4C997.695 1004.91 988.044 1021.34 987.476 1020.44C980.095 1009.99 975.838 1010.59 964.199 1001.92C954.832 989.976 950.574 964.587 941.207 952.639C920.201 955.626 889.828 1033.59 873.08 1052.1C872.134 1051.51 871.094 1050.91 869.958 1050.31C856.901 1000.73 761.24 836.446 727.46 815.836C701.724 810.857 675.987 805.979 650.251 801.199C625.555 829.576 614.201 871.393 588.369 898.873C561.97 926.652 526.772 944.873 501.508 973.846V986.69C510.308 1000.43 524.501 1003.42 538.694 1011.18C558.375 1020.94 577.961 1030.7 597.453 1040.46C603.414 1041.95 609.659 1042.25 615.62 1041.65C626.406 1040.75 636.909 1037.77 644.006 1035.68C675.514 1026.42 712.7 1035.08 741.653 1042.25C761.807 1047.33 773.162 1061.96 787.923 1071.52C815.741 1089.74 844.695 1095.71 869.958 1115.43C873.175 1119.81 876.298 1124.19 879.325 1128.57C907.144 1139.92 997.127 1035.98 998.546 1006.4Z" fill="#F3E2E7"/>
|
||||
<path d="M338.006 489.055C331.477 509.366 320.974 533.262 314.445 553.573C314.067 554.171 313.878 554.868 313.878 555.664C302.807 610.624 299.969 742.649 311.039 799.102C323.907 856.054 336.776 913.005 349.644 969.957C357.024 1013.27 305.078 1106.76 282.937 1111.84C283.505 1110.94 286.343 1097.5 288.898 1086.45C290.223 1080.28 291.074 1074.1 291.453 1067.93C293.913 1028.7 296.278 989.472 298.549 950.243C299.685 935.308 298.265 920.373 294.859 905.737C286.722 871.287 278.679 836.838 270.731 802.388C256.822 726.817 277.828 603.157 298.549 549.69C306.781 528.483 337.154 489.055 338.006 489.055Z" fill="#EC92AD"/>
|
||||
<path d="M531.597 416.177L631.799 146.752C631.799 146.752 707.874 188.271 796.154 392.281L531.597 416.177Z" fill="#EC92AD"/>
|
||||
<path d="M197.493 484.286C197.493 484.286 264.2 458.897 333.746 475.923C403.576 492.949 278.677 643.193 278.677 643.193C278.677 643.193 184.72 509.974 197.493 484.286Z" fill="#EC92AD"/>
|
||||
<path d="M527.906 374.657C527.906 374.657 525.635 421.254 537.557 422.449L632.083 147.05C632.083 146.751 631.799 146.751 631.799 147.05C510.591 379.138 532.448 346.58 527.906 374.657Z" fill="#F3E2E7"/>
|
||||
<path d="M784.231 376.753C769.47 367.194 751.587 313.13 750.451 308.948C741.368 274.3 719.511 246.819 674.377 195.444C651.384 169.158 630.947 147.951 631.798 146.756C632.65 145.86 651.101 161.392 661.603 170.652C686.299 192.158 709.008 215.755 737.11 252.793C788.772 320.299 800.695 369.882 810.63 386.012C810.914 386.61 812.333 388.999 811.481 389.895C810.346 391.389 803.817 387.804 796.153 383.623C790.76 380.636 785.65 377.649 784.231 376.753Z" fill="#F3E2E7"/>
|
||||
<path d="M563.389 412.297C560.834 410.504 573.324 383.323 592.627 352.258C608.523 326.57 610.226 328.661 624.987 303.272C633.218 289.532 640.599 275.792 642.302 276.688C643.438 276.987 642.018 281.467 640.883 287.74C634.922 318.207 642.586 333.441 639.747 355.245C639.747 356.44 639.18 360.024 640.315 360.323C641.45 360.921 644.573 355.245 647.695 350.466C650.818 345.986 655.927 341.207 665.862 331.947C685.449 313.428 687.436 314.025 688.287 314.623C689.991 315.817 688.004 319.7 688.287 334.038C688.287 335.233 688.287 346.284 692.829 355.245C693.397 356.141 694.248 357.635 695.668 358.531C697.655 359.726 700.493 360.025 707.306 356.739C717.525 351.661 718.944 346.583 722.351 347.479C725.189 348.375 724.621 352.258 729.731 362.115C733.421 369.583 745.911 371.674 744.775 374.063C743.356 377.05 726.608 377.946 724.905 378.245C665.862 388.401 598.02 406.323 579.285 408.115C577.866 408.413 574.176 408.712 569.634 410.206C565.092 411.699 563.957 412.894 563.389 412.297Z" fill="#F3E2E7"/>
|
||||
<path d="M275.975 638.954C275.704 642.789 275.704 642.789 274.809 646.076C272.822 646.673 271.865 646.183 259.943 625.274C247.453 603.469 242.06 596.301 230.421 573.301C219.918 551.795 213.957 537.159 210.267 531.782C210.267 531.782 200.332 514.458 194.939 495.938C194.087 492.951 193.803 489.666 193.803 489.666C193.803 487.276 193.519 484.588 194.087 483.692C196.074 481.004 232.976 500.12 267.039 525.211C276.407 532.379 297.696 548.808 304.225 550.003C304.509 550.301 305.36 550.301 305.928 551.197C308.199 554.184 299.115 560.756 295.993 570.613C288.613 592.119 288.613 611.235 286.058 616.612C282.651 623.482 280.381 627.963 279.529 627.963C278.677 627.963 278.394 612.729 278.394 612.729C278.394 612.729 267.891 615.118 266.471 613.028C263.065 607.352 284.638 590.028 281.516 584.353C279.245 580.171 265.052 585.846 263.917 582.859C262.214 579.275 280.664 572.704 280.664 563.145C280.664 550.003 244.898 544.029 226.731 523.717C226.163 522.821 225.028 521.626 224.176 521.925C220.77 523.717 244.047 578.379 254.833 601.08C266.471 624.975 267.234 620.481 272.06 628.545C272.627 629.442 273.274 630.737 274.142 633.476C274.751 635.397 275.704 637.311 275.975 638.954Z" fill="#F3E2E7"/>
|
||||
<path d="M1527.94 2023.17C1506.37 2056.62 1443.64 2099.33 1408.72 2120.84C1392.26 2131 1374.38 2128.01 1349.96 2132.19C1244.65 2150.11 1139.91 2126.51 1054.18 2099.63C1033.46 2093.06 982.082 2051.84 959.941 2060.5C928.149 2075.44 932.691 2115.76 916.511 2146.83C905.724 2170.72 877.338 2257.64 868.539 2277.06C846.114 2326.64 818.012 2377.72 791.045 2423.72C770.323 2459.26 717.242 2525.57 657.915 2490.63C630.097 2474.2 631.516 2451.5 627.258 2427.9C626.974 2427 626.974 2414.16 626.123 2414.16C623 2414.16 623 2414.16 619.31 2414.16C612.119 2423.32 604.833 2432.48 597.453 2441.64C556.293 2481.37 494.412 2471.81 479.935 2412.37C470.284 2373.24 488.167 2333.81 498.386 2308.12C517.499 2246.19 536.612 2184.26 555.725 2122.33C565.944 2070.96 542.668 2029.14 529.327 1995.39C504.347 1930.87 497.25 1854.1 471.987 1788.39C461.484 1760.61 441.33 1746.27 433.382 1726.56C424.014 1702.96 403.293 1606.18 386.829 1597.82C334.031 1572.73 319.838 1656.37 289.465 1667.72C227.584 1691.01 200.617 1599.31 196.643 1552.12C145.264 1534.2 106.66 1452.95 131.355 1377.98C153.213 1312.57 199.198 1265.37 286.343 1267.16C294.007 1257.01 305.929 1251.93 314.161 1242.67L331.193 1170.98C340.087 1153.66 348.886 1136.23 357.591 1118.71C369.797 1099.89 391.939 1076 410.106 1063.45C420.514 1058.08 430.827 1052.6 441.046 1047.02C435.369 1045.63 430.26 1043.94 425.718 1041.95C436.504 1038.66 446.156 1037.76 454.104 1037.76C494.128 1037.47 510.308 1061.36 554.306 1059.87C574.176 1058.97 574.176 1053.89 615.62 1041.65C678.069 1023.13 709.293 1013.87 741.086 1021.93C777.987 1031.19 772.026 1051.21 835.895 1081.67C869.39 1097.5 893.234 1108.56 918.782 1099.3C943.761 1090.04 961.645 1064.95 968.457 1055.39C978.676 1040.75 984.07 1027.91 985.773 1024.02C985.773 1024.02 988.895 1016.86 996.276 1002.82C1007.91 981.012 1029.77 943.675 1066.67 892C1105.85 837.936 1195.26 823.897 1257.99 821.209C1275.78 832.958 1293.57 844.707 1311.36 856.455C1368.7 900.961 1426.04 963.389 1466.06 1025.82C1535.89 1134.84 1586.14 1257.01 1617.93 1407.25C1664.48 1627.39 1615.37 1889.05 1527.94 2023.17Z" fill="url(#paint0_linear_526_1314)"/>
|
||||
<path d="M400.737 437.385C402.441 468.748 377.745 553.578 372.919 572.396C371.784 572.694 370.364 572.993 368.661 573.292C364.971 573.591 361.281 572.695 358.159 570.305L357.591 570.006C351.062 520.423 376.609 470.54 400.737 437.385Z" fill="#EC92AD"/>
|
||||
<path d="M572.473 531.775C622.148 554.178 644.289 624.67 674.662 668.579C711.28 721.448 752.156 775.213 783.096 833.161C787.354 846.303 822.269 917.99 835.894 940.691C868.822 999.833 900.898 1115.73 900.898 1204.74C899.763 1204.14 810.631 989.678 809.495 989.678C814.605 1096.61 794.451 1134.84 794.451 1133.65C779.122 1061.07 776.568 995.652 755.278 940.691C700.493 799.408 646.276 654.54 572.473 531.775Z" fill="#EC92AD"/>
|
||||
<path d="M363.552 610.025C377.745 625.259 381.719 691.57 388.248 717.556C398.656 753.997 408.97 790.339 419.189 826.581C439.343 875.268 473.974 903.943 471.987 966.67C470.473 966.67 468.864 966.67 467.161 966.67C384.842 933.215 349.643 739.361 363.552 610.025Z" fill="#EC92AD"/>
|
||||
<path d="M1035.16 823.002C1034.6 871.69 985.205 970.26 964.767 1006.1C961.361 1006.7 927.581 993.558 924.459 981.61C921.62 969.961 979.812 816.729 987.476 809.859C1001.67 814.937 1018.13 821.807 1035.16 823.002Z" fill="#EC92AD"/>
|
||||
<path d="M1310.22 1489.99C1234.72 1490.59 1138.2 1436.52 1084.84 1403.97C1082.85 1402.77 1081.72 1400.08 1082.57 1397.69C1083.14 1395.3 1083.99 1392.91 1084.56 1390.82C1085.41 1388.43 1087.39 1387.24 1089.66 1387.24C1124.3 1388.73 1136.79 1410.24 1160.35 1421.29C1199.23 1439.81 1255.44 1458.63 1306.82 1460.42C1309.66 1460.42 1312.21 1458.03 1312.21 1454.74C1312.78 1375.89 1304.55 1305.4 1283.82 1251.93C1283.64 1251.73 1283.54 1251.53 1283.54 1251.33C1282.12 1244.76 1278.43 1238.49 1276.73 1231.92C1276.73 1231.32 1276.16 1230.42 1275.59 1229.83C1271.62 1225.35 1264.81 1225.64 1261.12 1219.37C1257.43 1213.1 1284.11 1185.02 1287.8 1166.5C1301.42 1097.5 1236.7 990.569 1199.23 955.622C1194.69 957.115 1193.56 957.713 1189.02 959.206C1186.18 977.427 1176.53 983.401 1168.29 996.245C1136.22 1044.63 1119.75 1098.4 1041.98 1098.7C1039.99 1098.7 1038.57 1100.79 1037.15 1102.58C1014.73 1134.24 987.475 1161.72 966.754 1183.53C913.956 1239.38 833.056 1381.27 759.536 1400.08C707.874 1413.52 643.722 1396.5 581.272 1406.65C548.345 1412.03 418.905 1429.06 390.519 1423.08C387.964 1422.49 386.261 1419.8 386.545 1417.11C386.829 1412.03 384.842 1408.45 390.519 1405.46C414.079 1399.49 435.085 1409.64 455.806 1405.16C486.274 1397.99 516.742 1390.92 547.209 1383.95C547.209 1383.95 547.304 1383.95 547.493 1383.95C628.961 1369.62 703.332 1376.78 778.271 1361.85C779.974 1361.55 787.922 1353.19 789.058 1351.69C812.902 1327.5 824.824 1313.46 841.288 1295.24C864.848 1269.25 889.828 1244.16 911.117 1216.09C939.787 1178.75 961.077 1157.24 1009.05 1101.39C1012.17 1097.8 1013.87 1096.01 1013.59 1093.92C1013.59 1092.13 1013.59 1089.74 1010.18 1087.94C975.269 1069.13 993.153 1013.57 1003.94 981.608C1018.13 939.791 1109.53 865.714 1172.84 880.35C1258.28 900.662 1296.31 958.011 1319.87 1044.93C1325.55 1065.54 1336.62 1096.91 1330.95 1126.18C1330.76 1126.38 1330.66 1126.58 1330.66 1126.78C1326.12 1139.32 1324.13 1157.24 1319.59 1169.49C1319.59 1169.69 1319.59 1169.79 1319.59 1169.79C1313.63 1192.79 1311.08 1208.92 1295.46 1222.96C1293.48 1224.75 1292.91 1227.73 1294.33 1229.83C1325.55 1286.88 1338.89 1359.46 1341.45 1442.5C1343.15 1488.8 1339.46 1555.7 1347.13 1594.53C1347.41 1596.33 1348.83 1597.82 1350.25 1598.42C1415.54 1628.29 1581.31 1777.64 1600.61 1846.93C1600.8 1847.93 1600.8 1848.83 1600.61 1849.62C1596.07 1869.93 1589.54 1889.05 1583.01 1907.57C1582.82 1908.17 1582.44 1908.76 1581.88 1909.36C1537.6 1960.44 1447.61 2019.88 1350.25 1974.78C1294.9 1949.09 1257.71 1914.14 1220.24 1867.54C1194.69 1835.88 1169.71 1800.04 1145.02 1766.88C1103.86 1711.92 1070.93 1651.88 1032.04 1593.94C1015.01 1568.85 999.114 1539.57 980.947 1514.48C980.947 1514.28 980.852 1514.09 980.663 1513.89C968.741 1500.94 956.913 1488 945.18 1475.06C942.058 1469.08 944.045 1467.59 947.167 1464.6C947.924 1464 948.776 1463.61 949.722 1463.41C970.16 1459.82 991.166 1491.19 1001.1 1504.93C1001.1 1505.12 1001.2 1505.22 1001.38 1505.22C1019.84 1524.94 1035.16 1553.02 1052.2 1575.42C1052.38 1575.62 1052.48 1575.72 1052.48 1575.72C1068.38 1601.7 1083.99 1620.22 1100.17 1646.21C1161.77 1738.21 1229.32 1861.27 1319.87 1921.61C1344 1937.74 1392.54 1971.19 1434.55 1962.23C1481.39 1952.37 1512.62 1930.57 1545.26 1909.36C1560.87 1898.91 1566.27 1877.4 1557.18 1860.08C1509.21 1770.17 1411.85 1663.23 1322.15 1635.75C1319.87 1635.16 1318.46 1633.07 1318.17 1630.68C1317.23 1585.67 1316.28 1540.57 1315.33 1495.37C1315.33 1492.38 1313.06 1489.99 1310.22 1489.99Z" fill="#EC92AD"/>
|
||||
<path d="M558.564 1077.49C660.47 1073.61 587.234 1257.9 516.837 1232.51C469.432 1215.19 498.954 1132.15 518.256 1105.27C528.191 1091.83 543.803 1086.45 558.564 1077.49Z" fill="#EC92AD"/>
|
||||
<path d="M405.28 1092.13C483.341 1097.21 418.905 1245.66 363.552 1225.95C291.736 1200.26 371.5 1109.15 405.28 1092.13Z" fill="#EC92AD"/>
|
||||
<path d="M651.669 1134.55C684.88 1158.44 710.712 1254.62 687.151 1300.92C670.971 1304.8 599.439 1325.41 584.962 1305.7C584.962 1303.51 584.962 1301.32 584.962 1299.13C640.598 1277.03 653.372 1213.1 651.669 1134.55Z" fill="#EC92AD"/>
|
||||
<path d="M489.017 1145.89C486.179 1187.11 475.96 1246.85 471.986 1273.14C468.58 1294.34 478.231 1322.12 470.283 1336.76C460.631 1343.13 450.886 1349.6 441.045 1356.17C405.279 1351.1 396.479 1301.51 396.195 1259.99C422.594 1247.75 481.921 1179.34 489.017 1145.89Z" fill="#EC92AD"/>
|
||||
<path d="M200.9 1315.55C222.757 1314.95 255.968 1330.49 264.484 1344.82C186.991 1344.23 170.811 1429.06 159.172 1499.55C157.658 1498.95 156.144 1498.45 154.631 1498.06C107.226 1410.24 146.966 1368.12 200.9 1315.55Z" fill="#EC92AD"/>
|
||||
<path d="M396.196 1348.11C408.118 1352 416.35 1353.19 427.421 1361.55C431.679 1364.84 430.259 1367.83 425.718 1373.2C404.428 1392.32 374.339 1404.57 349.643 1401.88C335.166 1400.39 314.445 1399.19 305.077 1405.76C302.806 1406.96 303.09 1409.05 303.374 1410.84C310.754 1453.85 372.068 1563.47 355.604 1598.12C350.779 1598.42 349.359 1598.42 344.818 1598.42C324.947 1567.66 256.821 1448.18 290.884 1390.53C308.2 1360.96 370.081 1369.02 396.196 1348.11Z" fill="#EC92AD"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_526_1314" x1="37.4461" y1="2500.51" x2="37.4461" y2="821.087" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F3E2E7"/>
|
||||
<stop offset="1" stop-color="#F3E2E7"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_526_1314">
|
||||
<rect width="1500" height="1500" rx="375" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_526_1314">
|
||||
<rect width="1894" height="1993" fill="white" transform="translate(-83 -420)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { useAudioContext } from '@proj-airi/stage-ui/stores/audio'
|
||||
import { useTheme } from '@proj-airi/ui'
|
||||
import { useElementBounding } from '@vueuse/core'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode
|
||||
const analyser = ref<AnalyserNode>()
|
||||
const analyserDataBuffer = ref<Uint8Array<ArrayBuffer>>()
|
||||
const { audioContext } = useAudioContext()
|
||||
const canvasElemRef = ref<HTMLCanvasElement>()
|
||||
const { isDark } = useTheme()
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/AudioBufferSourceNode/playbackRate
|
||||
// explain: https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Visualizations_with_Web_Audio_API
|
||||
// reference: https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Simple_synth
|
||||
function fetchAnalyserDataDuringFrames() {
|
||||
if (!analyser.value || !analyserDataBuffer.value || !canvasElemRef.value)
|
||||
return
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame
|
||||
requestAnimationFrame(fetchAnalyserDataDuringFrames)
|
||||
if (analyserDataBuffer.value.length > 60 * 2)
|
||||
analyserDataBuffer.value = new Uint8Array(analyser.value.frequencyBinCount)
|
||||
|
||||
analyser.value.getByteTimeDomainData(analyserDataBuffer.value)
|
||||
|
||||
const context = canvasElemRef.value.getContext('2d')!
|
||||
|
||||
if (isDark.value)
|
||||
context.fillStyle = 'rgba(34, 34, 34, 1)'
|
||||
else
|
||||
context.fillStyle = 'rgba(255, 255, 255, 1)'
|
||||
|
||||
context.fillRect(0, 0, canvasElemRef.value.width, canvasElemRef.value.height)
|
||||
|
||||
context.lineWidth = 2
|
||||
if (isDark.value)
|
||||
context.strokeStyle = 'rgb(255 255 255)'
|
||||
else
|
||||
context.strokeStyle = 'rgb(0 0 0)'
|
||||
|
||||
context.beginPath()
|
||||
|
||||
const sliceWidth = (canvasElemRef.value.width * 1.0) / analyser.value.frequencyBinCount
|
||||
let x = 0
|
||||
|
||||
for (let i = 0; i < analyser.value.frequencyBinCount; i++) {
|
||||
const v = analyserDataBuffer.value[i] / 128.0
|
||||
const y = (v * canvasElemRef.value.height) / 2
|
||||
|
||||
if (i === 0)
|
||||
context.moveTo(x, y)
|
||||
else
|
||||
context.lineTo(x, y)
|
||||
|
||||
x += sliceWidth
|
||||
}
|
||||
|
||||
context.lineTo(canvasElemRef.value.width, canvasElemRef.value.height / 2)
|
||||
context.stroke()
|
||||
}
|
||||
|
||||
function initAnalyser() {
|
||||
analyser.value = audioContext.createAnalyser()
|
||||
analyserDataBuffer.value = new Uint8Array(analyser.value.frequencyBinCount)
|
||||
analyser.value.getByteTimeDomainData(analyserDataBuffer.value)
|
||||
const windowAny = window as any
|
||||
windowAny.analyserDataBuffer = analyserDataBuffer
|
||||
|
||||
fetchAnalyserDataDuringFrames()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
analyser: () => analyser.value,
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (!containerRef.value || !canvasElemRef.value)
|
||||
return
|
||||
|
||||
const containerElementBounding = useElementBounding(containerRef.value)
|
||||
containerElementBounding.update()
|
||||
|
||||
initAnalyser()
|
||||
|
||||
canvasElemRef.value.width = containerElementBounding.width.value
|
||||
canvasElemRef.value.height = containerElementBounding.height.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" h="[80px]" w-full>
|
||||
<canvas ref="canvasElemRef" h-full w-full />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { BackgroundPickerDialog } from '@proj-airi/stage-ui/components'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import { useBackgroundStore } from '../../stores/background'
|
||||
|
||||
const show = defineModel<boolean>({ default: false })
|
||||
|
||||
const backgroundStore = useBackgroundStore()
|
||||
const { options, selectedOption } = storeToRefs(backgroundStore)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BackgroundPickerDialog
|
||||
v-model="show"
|
||||
:selected="selectedOption"
|
||||
:options="options"
|
||||
@apply="backgroundStore.applyPickerSelection"
|
||||
@remove="option => backgroundStore.removeOption(option.id)"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="line-1-background-container">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
/*
|
||||
Pattern Monster - SVG Pattern Generator
|
||||
https://pattern.monster/
|
||||
*/
|
||||
|
||||
.line-1-background-container {
|
||||
background-image: url("data:image/svg+xml,<svg id='patternId' width='100%' height='100%' xmlns='http://www.w3.org/2000/svg'><defs><pattern id='a' patternUnits='userSpaceOnUse' width='40' height='40' patternTransform='scale(8) rotate(45)'><rect x='0' y='0' width='100%' height='100%' fill='%23ffffffff'/><path d='M20-5V5m0 30v10m20-30v10M0 15v10' stroke-linejoin='round' stroke-linecap='round' stroke-width='2' stroke='%23eee7e9ff' fill='none'/><path d='M-5 40H5M-5 0H5m30 0h10M35 40h10M15 20h10' stroke-linejoin='round' stroke-linecap='round' stroke-width='2' stroke='%23efbcceff' fill='none'/></pattern></defs><rect width='800%' height='800%' transform='translate(-488,-456)' fill='url(%23a)'/></svg>");
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import type { BackgroundItem } from '../../stores/background'
|
||||
|
||||
import { BackgroundGradientOverlay } from '@proj-airi/stage-ui/components'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { BackgroundKind } from '../../stores/background'
|
||||
import { DefaultBackground } from '../Backgrounds/default'
|
||||
|
||||
defineProps<{
|
||||
background: BackgroundItem
|
||||
topColor?: string
|
||||
}>()
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
defineExpose({
|
||||
surfaceEl: containerRef,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" class="customized-background relative min-h-100dvh w-full overflow-hidden">
|
||||
<!-- Background layers -->
|
||||
<div
|
||||
class="absolute inset-0 z-0 transition-all duration-300"
|
||||
:class="[(background.blur && background.kind !== BackgroundKind.Wave) ? 'blur-md scale-110' : '']"
|
||||
>
|
||||
<template v-if="background.kind === BackgroundKind.Wave">
|
||||
<DefaultBackground class="h-full w-full" />
|
||||
</template>
|
||||
<template v-else-if="background.kind === BackgroundKind.Image">
|
||||
<img
|
||||
:src="background.src"
|
||||
class="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="h-full w-full bg-neutral-950" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Overlay (not for wave) -->
|
||||
<BackgroundGradientOverlay v-if="background.kind !== BackgroundKind.Wave" :color="topColor" />
|
||||
|
||||
<!-- Content layer (kept mounted during background switches) -->
|
||||
<div class="relative z-10 h-full w-full">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
interface Props {
|
||||
petalCount?: number
|
||||
refreshInterval?: number
|
||||
baseColor?: string
|
||||
}
|
||||
|
||||
interface Petal {
|
||||
id: number
|
||||
style: {
|
||||
left: string
|
||||
top: string
|
||||
width: string
|
||||
height: string
|
||||
animationDuration: string
|
||||
animationDelay: string
|
||||
transform: string
|
||||
}
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
petalCount: 20,
|
||||
refreshInterval: 30000,
|
||||
baseColor: '#FFB6C1',
|
||||
})
|
||||
|
||||
const petals = ref<Petal[]>([])
|
||||
let animationInterval: number | null = null
|
||||
|
||||
function createPetalStyle() {
|
||||
const size = 8 + Math.random() * 4
|
||||
return {
|
||||
left: `${Math.random() * 100}%`,
|
||||
top: `-${size}px`,
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
animationDuration: `${5 + Math.random() * 5}s`,
|
||||
animationDelay: `${Math.random() * 5}s`,
|
||||
transform: `rotate(${Math.random() * 360}deg)`,
|
||||
}
|
||||
}
|
||||
|
||||
function createPetals() {
|
||||
petals.value.length = 0
|
||||
for (let i = 0; i < props.petalCount; i++) {
|
||||
petals.value.push({
|
||||
id: Date.now() + i,
|
||||
style: createPetalStyle(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
createPetals()
|
||||
animationInterval = window.setInterval(createPetals, props.refreshInterval)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (animationInterval) {
|
||||
window.clearInterval(animationInterval)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sakura-container">
|
||||
<div class="sakura-bg" />
|
||||
<div class="pattern-overlay" />
|
||||
<div class="petals-container">
|
||||
<div
|
||||
v-for="petal in petals"
|
||||
:key="petal.id"
|
||||
class="sakura-petal"
|
||||
:style="petal.style"
|
||||
/>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sakura-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sakura-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.pattern-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M30 5c1.4-1.4 3.8-1.4 5.2 0L45 14.8c1.4 1.4 1.4 3.8 0 5.2L35.2 30l9.8 9.8c1.4 1.4 1.4 3.8 0 5.2L35.2 55c-1.4 1.4-3.8 1.4-5.2 0L20.2 45c-1.4-1.4-1.4-3.8 0-5.2L30 30l-9.8-9.8c-1.4-1.4-1.4-3.8 0-5.2L30 5z' fill='%23FFB6C1' fill-opacity='0.1'/%3E%3C/svg%3E");
|
||||
opacity: 0.5;
|
||||
animation: patternFloat 20s linear infinite;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.petals-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sakura-petal {
|
||||
position: absolute;
|
||||
background: v-bind('props.baseColor');
|
||||
border-radius: 150% 0 150% 0;
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
filter: blur(1px);
|
||||
animation-name: fall, drift;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@keyframes fall {
|
||||
0% {
|
||||
top: -10px;
|
||||
transform: translateX(0) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
top: 100vh;
|
||||
transform: translateX(100px) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes drift {
|
||||
0% {
|
||||
transform: translateX(0) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(50px) rotate(180deg);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes patternFloat {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 60px 60px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="tic-tac-toe-background-container">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/*
|
||||
Hero Patterns | Free repeatable SVG background patterns for your web projects
|
||||
https://heropatterns.com/
|
||||
*/
|
||||
.tic-tac-toe-background-container {
|
||||
background-color: transparent;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='64' height='64' viewBox='0 0 64 64' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 16c4.418 0 8-3.582 8-8s-3.582-8-8-8-8 3.582-8 8 3.582 8 8 8zm0-2c3.314 0 6-2.686 6-6s-2.686-6-6-6-6 2.686-6 6 2.686 6 6 6zm33.414-6l5.95-5.95L45.95.636 40 6.586 34.05.636 32.636 2.05 38.586 8l-5.95 5.95 1.414 1.414L40 9.414l5.95 5.95 1.414-1.414L41.414 8zM40 48c4.418 0 8-3.582 8-8s-3.582-8-8-8-8 3.582-8 8 3.582 8 8 8zm0-2c3.314 0 6-2.686 6-6s-2.686-6-6-6-6 2.686-6 6 2.686 6 6 6zM9.414 40l5.95-5.95-1.414-1.414L8 38.586l-5.95-5.95L.636 34.05 6.586 40l-5.95 5.95 1.414 1.414L8 41.414l5.95 5.95 1.414-1.414L9.414 40z' fill='%23ffcfe1' fill-opacity='0.4' fill-rule='evenodd'/%3E%3C/svg%3E");
|
||||
background-position: 0 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.tic-tac-toe-background-container:hover {
|
||||
transform: scale(1.02);
|
||||
animation: slideBackground 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes slideBackground {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 64px -64px; /* Match the SVG size for smooth looping */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as DefaultBackground } from './index.vue'
|
||||
export { default as PartAnimatedWave } from './part-animated-wave.vue'
|
||||
export { default as PatternCross } from './pattern-cross.vue'
|
||||
export { default as DefaultBackgroundPreview } from './preview.vue'
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { useTheme } from '@proj-airi/ui'
|
||||
|
||||
import { PartAnimatedWave, PatternCross } from '.'
|
||||
|
||||
const { isDark } = useTheme()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PatternCross>
|
||||
<PartAnimatedWave
|
||||
:fill-color="isDark
|
||||
? 'oklch(35% calc(var(--chromatic-chroma) * 0.6) var(--chromatic-hue))'
|
||||
: 'color-mix(in srgb, oklch(95% calc(var(--chromatic-chroma-50) * 0.5) var(--chromatic-hue)) 80%, oklch(100% 0 360))'"
|
||||
class="h-full w-full"
|
||||
>
|
||||
<slot />
|
||||
</PartAnimatedWave>
|
||||
</PatternCross>
|
||||
</template>
|
||||
@@ -0,0 +1,138 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
type WaveDirection = 'up' | 'down'
|
||||
type MovementDirection = 'left' | 'right'
|
||||
|
||||
interface WaveProps {
|
||||
height?: number // Height of the wave in pixels
|
||||
amplitude?: number // Wave height variation in pixels
|
||||
waveLength?: number // Length of one wave cycle in pixels
|
||||
fillColor?: string // Fill color of the wave
|
||||
direction?: WaveDirection // Direction of the wave: 'up' or 'down'
|
||||
movementDirection?: MovementDirection // Direction of the wave movement: 'left' or 'right'
|
||||
animationSpeed?: number // Speed of the wave animation in pixels per second
|
||||
}
|
||||
|
||||
// Use either provided wave props or defaults
|
||||
const props = withDefaults(defineProps<WaveProps>(), {
|
||||
height: 40,
|
||||
amplitude: 14,
|
||||
waveLength: 250,
|
||||
fillColor: 'oklch(95% 0.10 var(--chromatic-hue))',
|
||||
direction: 'down',
|
||||
movementDirection: 'left',
|
||||
animationSpeed: 50,
|
||||
})
|
||||
|
||||
// Reactive Variables
|
||||
const waveHeight = ref(props.height)
|
||||
const waveAmplitude = ref(props.amplitude)
|
||||
const waveLength = ref(props.waveLength)
|
||||
const waveFillColor = ref(props.fillColor)
|
||||
const direction = ref<WaveDirection>(props.direction)
|
||||
const movementDirection = ref<MovementDirection>(props.movementDirection)
|
||||
|
||||
// Function to generate the SVG sine wave path
|
||||
function generateSineWavePath(
|
||||
width: number,
|
||||
height: number,
|
||||
amplitude: number,
|
||||
waveLength: number,
|
||||
direction: WaveDirection,
|
||||
): string {
|
||||
const points: string[] = []
|
||||
|
||||
// Calculate the number of complete waves to fill the SVG width
|
||||
const numberOfWaves = Math.ceil(width / waveLength)
|
||||
|
||||
// Total width covered by all complete waves
|
||||
const totalWavesWidth = numberOfWaves * waveLength
|
||||
|
||||
// Step size in pixels for generating points (1px for precision)
|
||||
const step = 1
|
||||
|
||||
// Determine base Y position based on direction
|
||||
const baseY = direction === 'up' ? amplitude : height - amplitude
|
||||
|
||||
// Start the path at the base Y position
|
||||
points.push(`M 0 ${baseY}`)
|
||||
|
||||
// Generate points for the sine wave
|
||||
const factor = Math.PI * 2 / waveLength
|
||||
for (let x = 0; x <= totalWavesWidth; x += step) {
|
||||
const deltaY = amplitude * Math.sin(factor * x)
|
||||
const y = direction === 'up' ? baseY - deltaY : baseY + deltaY
|
||||
points.push(`L ${x} ${y}`)
|
||||
}
|
||||
|
||||
// Close the path for filling
|
||||
const closeY = direction === 'up' ? height : 0
|
||||
points.push(`L ${totalWavesWidth} ${closeY}`)
|
||||
points.push(`L 0 ${closeY} Z`)
|
||||
|
||||
return points.join(' ')
|
||||
}
|
||||
|
||||
const fullHeight = computed(() => waveHeight.value + waveAmplitude.value * 2)
|
||||
|
||||
// Using `mask-image` rather than `background-image` here as we cannot directly control SVG's fill color
|
||||
const maskImage = computed(() => {
|
||||
const svg = `<svg width="${waveLength.value}" height="${fullHeight.value}" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="${generateSineWavePath(waveLength.value, fullHeight.value, waveAmplitude.value, waveLength.value, direction.value)}"/>
|
||||
</svg>`
|
||||
return `url(data:image/svg+xml;base64,${btoa(svg)})`
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [props.height, props.amplitude, props.waveLength, props.fillColor, props.direction, props.movementDirection],
|
||||
() => {
|
||||
waveHeight.value = props.height!
|
||||
waveAmplitude.value = props.amplitude!
|
||||
waveLength.value = props.waveLength!
|
||||
waveFillColor.value = props.fillColor!
|
||||
direction.value = props.direction!
|
||||
movementDirection.value = props.movementDirection!
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative">
|
||||
<slot />
|
||||
<div absolute left-0 right-0 top-0 w-full overflow-hidden>
|
||||
<div
|
||||
class="colored-area wave"
|
||||
:style="{
|
||||
'background': waveFillColor,
|
||||
'height': `${fullHeight}px`,
|
||||
maskImage,
|
||||
'WebkitMaskImage': maskImage,
|
||||
'--wave-translate': `${-waveLength}px`,
|
||||
'--animation-duration': `${waveLength / animationSpeed}s`,
|
||||
'animation-direction': movementDirection === 'left' ? 'normal' : 'reverse',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@keyframes wave-animation {
|
||||
from {
|
||||
transform: translate(0);
|
||||
}
|
||||
to {
|
||||
transform: translate(var(--wave-translate, -250px));
|
||||
}
|
||||
}
|
||||
|
||||
.wave {
|
||||
width: 200vw;
|
||||
mask-repeat: repeat-x;
|
||||
-webkit-mask-repeat: repeat-x;
|
||||
will-change: transform;
|
||||
animation: wave-animation var(--animation-duration, 5s) linear infinite;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="cross-background-container">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
/*
|
||||
CSS Background Patterns by MagicPattern
|
||||
https://www.magicpattern.design/tools/css-backgrounds
|
||||
*/
|
||||
.cross-background-container {
|
||||
background-color: #ffffff;
|
||||
--cross-color: oklch(0.85 calc(var(--chromatic-chroma) * 0.3) var(--chromatic-hue) / 0.2);
|
||||
background:
|
||||
radial-gradient(circle, transparent 20%, #ffffff 20%, #ffffff 80%, transparent 80%, transparent),
|
||||
radial-gradient(circle, transparent 20%, #ffffff 20%, #ffffff 80%, transparent 80%, transparent) 25px 25px,
|
||||
linear-gradient(var(--cross-color) 2px, transparent 2px) 0 -1px,
|
||||
linear-gradient(90deg, var(--cross-color) 2px, #ffffff 2px) -1px 0;
|
||||
background-size:
|
||||
50px 50px,
|
||||
50px 50px,
|
||||
25px 25px,
|
||||
25px 25px;
|
||||
}
|
||||
|
||||
.dark .cross-background-container {
|
||||
background-color: #121212;
|
||||
--cross-color: oklch(0.2 calc(var(--chromatic-chroma) * 0.3) var(--chromatic-hue));
|
||||
background:
|
||||
radial-gradient(circle, transparent 20%, #121212 20%, #121212 80%, transparent 80%, transparent),
|
||||
radial-gradient(circle, transparent 20%, #121212 20%, #121212 80%, transparent 80%, transparent) 25px 25px,
|
||||
linear-gradient(var(--cross-color) 2px, transparent 2px) 0 -1px,
|
||||
linear-gradient(90deg, var(--cross-color) 2px, #121212 2px) -1px 0;
|
||||
background-size:
|
||||
50px 50px,
|
||||
50px 50px,
|
||||
25px 25px,
|
||||
25px 25px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { useTheme } from '@proj-airi/ui'
|
||||
|
||||
import { PartAnimatedWave, PatternCross } from '.'
|
||||
|
||||
const { isDark } = useTheme()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PatternCross>
|
||||
<PartAnimatedWave
|
||||
:fill-color="isDark
|
||||
? 'oklch(35% calc(var(--chromatic-chroma) * 0.6) var(--chromatic-hue))'
|
||||
: 'color-mix(in srgb, oklch(95% calc(var(--chromatic-chroma-50) * 0.5) var(--chromatic-hue)) 80%, oklch(100% 0 360))'"
|
||||
class="h-full w-full"
|
||||
>
|
||||
<div class="relative h-full w-full from-black/10 via-black/0 to-black/0 bg-gradient-to-b" />
|
||||
</PartAnimatedWave>
|
||||
</PatternCross>
|
||||
</template>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as BackgroundDialogPicker } from './DialogPicker.vue'
|
||||
export { default as BackgroundProvider } from './Provider.vue'
|
||||
@@ -0,0 +1,204 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
values: number[]
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
disabled?: boolean
|
||||
}>(), {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:values', value: number[]): void
|
||||
(e: 'mousedown', event: MouseEvent): void
|
||||
}>()
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(value, min), max)
|
||||
}
|
||||
|
||||
const sliderRef = ref<HTMLElement | null>(null)
|
||||
const isDragging = ref(false)
|
||||
const previousIndex = ref<number>(0)
|
||||
|
||||
// Utility functions
|
||||
const asc = (a: number, b: number) => a - b
|
||||
|
||||
function findClosest(values: number[], currentValue: number) {
|
||||
const { index: closestIndex } = values.reduce((acc: { distance: number, index: number } | null, value: number, index: number) => {
|
||||
const distance = Math.abs(currentValue - value)
|
||||
if (acc === null || distance < acc.distance || distance === acc.distance) {
|
||||
return { distance, index }
|
||||
}
|
||||
return acc
|
||||
}, null) || { index: 0 }
|
||||
return closestIndex
|
||||
}
|
||||
|
||||
function valueToPercent(value: number, min: number, max: number) {
|
||||
return ((value - min) * 100) / (max - min)
|
||||
}
|
||||
|
||||
function percentToValue(percent: number, min: number, max: number) {
|
||||
return (max - min) * percent + min
|
||||
}
|
||||
|
||||
function getDecimalPrecision(num: number) {
|
||||
if (Math.abs(num) < 1) {
|
||||
const parts = num.toExponential().split('e-')
|
||||
const matissaDecimalPart = parts[0].split('.')[1]
|
||||
return (matissaDecimalPart ? matissaDecimalPart.length : 0) + Number.parseInt(parts[1], 10)
|
||||
}
|
||||
const decimalPart = num.toString().split('.')[1]
|
||||
return decimalPart ? decimalPart.length : 0
|
||||
}
|
||||
|
||||
function roundValueToStep(value: number, step: number) {
|
||||
const nearest = Math.round(value / step) * step
|
||||
return Number(nearest.toFixed(getDecimalPrecision(step)))
|
||||
}
|
||||
|
||||
// Computed values
|
||||
const sortedValues = computed(() => {
|
||||
return [...props.values]
|
||||
.sort(asc)
|
||||
.map(value => clamp(value, props.min, props.max))
|
||||
})
|
||||
|
||||
const sliderStyle = computed(() => {
|
||||
const sliderOffset = valueToPercent(sortedValues.value[0], props.min, props.max)
|
||||
const sliderLeap = valueToPercent(sortedValues.value[sortedValues.value.length - 1], props.min, props.max) - sliderOffset
|
||||
return {
|
||||
left: `${sliderOffset}%`,
|
||||
width: `${sliderLeap}%`,
|
||||
backgroundSize: `${sliderLeap}% 100%`,
|
||||
}
|
||||
})
|
||||
|
||||
// Event handlers
|
||||
function getNewValue(event: MouseEvent, move = false) {
|
||||
if (!sliderRef.value)
|
||||
return { newValue: sortedValues.value, activeIndex: 0 }
|
||||
|
||||
const { width, left } = sliderRef.value.getBoundingClientRect()
|
||||
const percent = (event.clientX - left) / width
|
||||
|
||||
let currentValue = percentToValue(percent, props.min, props.max)
|
||||
currentValue = roundValueToStep(currentValue, props.step)
|
||||
currentValue = clamp(currentValue, props.min, props.max)
|
||||
|
||||
const activeIndex = move ? previousIndex.value : findClosest(sortedValues.value, currentValue)
|
||||
|
||||
const newValues = [...sortedValues.value]
|
||||
newValues[activeIndex] = currentValue
|
||||
const sortedNewValues = [...newValues].sort(asc)
|
||||
|
||||
const newActiveIndex = sortedNewValues.indexOf(currentValue)
|
||||
previousIndex.value = newActiveIndex
|
||||
|
||||
return {
|
||||
newValue: sortedNewValues,
|
||||
activeIndex: newActiveIndex,
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseDown(event: MouseEvent) {
|
||||
if (props.disabled)
|
||||
return
|
||||
|
||||
event.preventDefault()
|
||||
isDragging.value = true
|
||||
emit('mousedown', event)
|
||||
|
||||
const { newValue } = getNewValue(event)
|
||||
emit('update:values', newValue)
|
||||
}
|
||||
|
||||
function handleMouseMove(event: MouseEvent) {
|
||||
if (!isDragging.value || props.disabled)
|
||||
return
|
||||
|
||||
const { newValue } = getNewValue(event, true)
|
||||
emit('update:values', newValue)
|
||||
}
|
||||
|
||||
function handleMouseUp(_: MouseEvent) {
|
||||
if (!isDragging.value)
|
||||
return
|
||||
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
function handleMouseLeave(event: MouseEvent) {
|
||||
if (!isDragging.value)
|
||||
return
|
||||
handleMouseUp(event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
ref="sliderRef"
|
||||
class="range-slider"
|
||||
:class="{ disabled }"
|
||||
@mousedown="handleMouseDown"
|
||||
@mousemove="handleMouseMove"
|
||||
@mouseup="handleMouseUp"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<span
|
||||
class="slider-track"
|
||||
:style="sliderStyle"
|
||||
/>
|
||||
<span
|
||||
v-for="(value, index) in sortedValues"
|
||||
:key="index"
|
||||
role="slider"
|
||||
class="slider-thumb"
|
||||
:style="{ left: `${valueToPercent(value, min, max)}%` }"
|
||||
:data-index="index"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.range-slider {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
cursor: ew-resize;
|
||||
touch-action: none;
|
||||
border: 3px solid #4bb9fd;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.range-slider.disabled {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.slider-track {
|
||||
display: block;
|
||||
position: relative;
|
||||
background-color: #4bb9fd;
|
||||
background-image: linear-gradient(90deg, var(--primary-light), var(--primary-light));
|
||||
background-repeat: no-repeat;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.slider-thumb {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: white;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
icon: string
|
||||
iconSize: number // avoid conflict with unocss size prop
|
||||
position: string
|
||||
duration: number
|
||||
started: boolean
|
||||
textColor: string
|
||||
isReverse?: boolean
|
||||
zIndex?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'animationEnded'): void
|
||||
}>()
|
||||
|
||||
const isAnimating = ref(false)
|
||||
|
||||
watch(() => props.started, (newVal) => {
|
||||
if (newVal) {
|
||||
requestAnimationFrame(() => {
|
||||
isAnimating.value = true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const clsAndProps = computed(() => {
|
||||
return {
|
||||
opacity: isAnimating.value !== props.isReverse ? 1 : 0, // this equals to opacity: isAnimating.value ? props.isReverse ? 1 : 0 : props.isReverse ? 0 : 1
|
||||
size: isAnimating.value !== props.isReverse ? 25 : props.iconSize,
|
||||
position: isAnimating.value !== props.isReverse ? `calc(50dvw - 12.5rem), calc(50dvh - 12.5rem)` : props.position,
|
||||
textColor: isAnimating.value !== props.isReverse ? 'text-white' : props.textColor,
|
||||
}
|
||||
})
|
||||
|
||||
const animationEndProps = ref<string[]>([])
|
||||
const animationEnded = ref(false)
|
||||
function handleAnimationEnded(e: TransitionEvent) {
|
||||
animationEndProps.value.push(e.propertyName)
|
||||
if (animationEndProps.value.includes('color') && animationEndProps.value.includes('width') && animationEndProps.value.includes('height') && animationEndProps.value.includes('transform')) {
|
||||
animationEnded.value = true
|
||||
emit('animationEnded')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
pointer-events-none fixed w="100dvw" h="100dvh"
|
||||
:style="{
|
||||
zIndex: animationEnded ? zIndex : undefined,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
fixed inset-0 bg-primary-500 transition-opacity ease-linear :style="{
|
||||
opacity: clsAndProps.opacity,
|
||||
transitionDuration: `${duration}ms`,
|
||||
}"
|
||||
/>
|
||||
<div
|
||||
fixed inset-0 ease-in-out :style="{
|
||||
width: `${clsAndProps.size}rem`,
|
||||
height: `${clsAndProps.size}rem`,
|
||||
transform: `translate(${clsAndProps.position})`,
|
||||
transitionDuration: `${duration}ms`,
|
||||
}" :class="[
|
||||
clsAndProps.textColor,
|
||||
props.icon,
|
||||
{ 'transition-all': isAnimating },
|
||||
]"
|
||||
@transitionend="handleAnimationEnded"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import HeaderLink from './HeaderLink.vue'
|
||||
import ActionAbout from './InteractiveArea/Actions/About.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header mb-1 w-full flex items-center justify-between gap-2>
|
||||
<HeaderLink />
|
||||
<div flex items-center gap-2>
|
||||
<ActionAbout />
|
||||
<RouterLink
|
||||
border="2 solid neutral-100/60 dark:neutral-800/30"
|
||||
bg="neutral-50/70 dark:neutral-800/70"
|
||||
w-fit flex items-center justify-center rounded-xl p-2 backdrop-blur-md
|
||||
title="Settings"
|
||||
to="/settings"
|
||||
>
|
||||
<div i-solar:settings-minimalistic-bold-duotone size-5 text="neutral-500 dark:neutral-400" />
|
||||
</RouterLink>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { useTheme } from '@proj-airi/ui'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import LogoDark from '../../assets/logo-dark.svg'
|
||||
import Logo from '../../assets/logo.svg'
|
||||
|
||||
const { isDark: dark } = useTheme()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterLink
|
||||
to="/" flex="~" items-center
|
||||
gap-2 px-2 text-nowrap text-2xl outline-none
|
||||
>
|
||||
<template v-if="dark">
|
||||
<img :src="LogoDark" h-8 w-8 class="theme-colored">
|
||||
</template>
|
||||
<template v-else>
|
||||
<img :src="Logo" h-8 w-8 class="theme-colored">
|
||||
</template>
|
||||
<div translate-y="[2px]" font-semibold font-quicksand>
|
||||
<span>AIRI</span>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.theme-colored {
|
||||
filter: hue-rotate(calc(var(--chromatic-hue, 0) * 1deg));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChatHistoryItem } from '@proj-airi/stage-ui/types/chat'
|
||||
|
||||
import { ChatHistory } from '@proj-airi/stage-ui/components'
|
||||
import { useChatStore } from '@proj-airi/stage-ui/stores/chat'
|
||||
import { useDeferredMount } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import ChatActionButtons from '../Widgets/ChatActionButtons.vue'
|
||||
import ChatArea from '../Widgets/ChatArea.vue'
|
||||
import ChatContainer from '../Widgets/ChatContainer.vue'
|
||||
|
||||
const { isReady } = useDeferredMount()
|
||||
const { messages, sending, streamingMessage } = storeToRefs(useChatStore())
|
||||
|
||||
const isLoading = ref(true)
|
||||
const historyMessages = computed(() => messages.value as unknown as ChatHistoryItem[])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="col" items-center pt-4>
|
||||
<div h-full max-h="[85vh]" w-full py="4">
|
||||
<ChatContainer>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
absolute left-0 top-0 h-1 w-full overflow-hidden rounded-t-xl
|
||||
class="bg-primary-500/20"
|
||||
>
|
||||
<div h-full w="1/3" origin-left bg-primary-500 class="animate-scan" />
|
||||
</div>
|
||||
<div w="full" max-h="<md:[60%]" py="<sm:2" flex="~ col" rounded="lg" relative h-full flex-1 overflow-hidden px="2 <md:0" py-4>
|
||||
<ChatHistory
|
||||
v-if="isReady"
|
||||
:messages="historyMessages"
|
||||
:sending="sending"
|
||||
:streaming-message="streamingMessage"
|
||||
h-full
|
||||
variant="desktop"
|
||||
@vue:mounted="isLoading = false"
|
||||
/>
|
||||
</div>
|
||||
<ChatArea />
|
||||
</ChatContainer>
|
||||
</div>
|
||||
|
||||
<ChatActionButtons />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@keyframes scan {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(400%);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-scan {
|
||||
animation: scan 2s infinite linear;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { AboutContent, AboutDialog } from '@proj-airi/stage-ui/components'
|
||||
import { useBuildInfo } from '@proj-airi/stage-ui/composables'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const show = ref(false)
|
||||
const buildInfo = useBuildInfo()
|
||||
|
||||
const aboutLinks = [
|
||||
{ label: 'Home', href: 'https://airi.moeru.ai/docs/', icon: 'i-solar:home-smile-outline' },
|
||||
{ label: 'Documentations', href: 'https://airi.moeru.ai/docs/en/docs/overview/', icon: 'i-solar:document-add-outline' },
|
||||
{ label: 'GitHub', href: 'https://github.com/moeru-ai/airi', icon: 'i-simple-icons:github' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button border="2 solid neutral-100/60 dark:neutral-800/30" bg="neutral-50/70 dark:neutral-800/70" w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md title="About" @click="show = !show">
|
||||
<div i-solar:info-circle-outline size-5 text="neutral-500 dark:neutral-400" />
|
||||
</button>
|
||||
<AboutDialog v-model="show">
|
||||
<AboutContent subtitle="Web ver." :build-info="buildInfo" :links="aboutLinks" />
|
||||
</AboutDialog>
|
||||
</template>
|
||||
@@ -0,0 +1,73 @@
|
||||
<script lang="ts" setup>
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { Button } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'reset'): void
|
||||
}>()
|
||||
|
||||
const { stageModelRenderer, stageViewControlsEnabled } = storeToRefs(useSettings())
|
||||
|
||||
const mode = defineModel<'x' | 'y' | 'z' | 'scale'>({ required: true })
|
||||
|
||||
function handleViewControlsToggle(targetMode: 'x' | 'y' | 'z' | 'scale') {
|
||||
if (mode.value === targetMode) {
|
||||
emits('reset')
|
||||
return
|
||||
}
|
||||
|
||||
mode.value = targetMode
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div w-full flex flex-1 items-center self-end justify-end gap-2>
|
||||
<Transition name="fade">
|
||||
<div v-if="stageViewControlsEnabled" w-full flex justify-between gap-2>
|
||||
<Button variant="secondary-muted" :toggled="mode === 'x'" w-full @click="handleViewControlsToggle('x')">
|
||||
X
|
||||
</Button>
|
||||
<Button variant="secondary-muted" :toggled="mode === 'y'" w-full @click="handleViewControlsToggle('y')">
|
||||
Y
|
||||
</Button>
|
||||
<Button v-if="stageModelRenderer === 'vrm'" variant="secondary-muted" :toggled="mode === 'z'" w-full @click="handleViewControlsToggle('z')">
|
||||
Z
|
||||
</Button>
|
||||
<Button variant="secondary-muted" :toggled="mode === 'scale'" w-full @click="handleViewControlsToggle('scale')">
|
||||
Scale
|
||||
</Button>
|
||||
</div>
|
||||
</Transition>
|
||||
<button
|
||||
w-fit flex items-center self-end justify-center justify-self-end rounded-xl p-2 backdrop-blur-md
|
||||
border="2 solid neutral-100/60 dark:neutral-800/30"
|
||||
bg="neutral-50/70 dark:neutral-800/70"
|
||||
title="View"
|
||||
text="neutral-500 dark:neutral-400"
|
||||
@click="stageViewControlsEnabled = !stageViewControlsEnabled"
|
||||
>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="!stageViewControlsEnabled" i-solar:tuning-outline size-5 />
|
||||
<div v-else i-solar:alt-arrow-right-outline size-5 />
|
||||
</Transition>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-enter-to,
|
||||
.fade-leave-from {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import MobileHeaderLink from './MobileHeaderLink.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header mb-1 w-full gap-2>
|
||||
<div w-full flex justify-center>
|
||||
<MobileHeaderLink />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { useTheme } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import LogoDark from '../../assets/logo-dark.svg'
|
||||
import Logo from '../../assets/logo.svg'
|
||||
|
||||
import { BackgroundKind, useBackgroundStore } from '../../stores/background'
|
||||
|
||||
const { isDark: dark } = useTheme()
|
||||
const { selectedOption } = storeToRefs(useBackgroundStore())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterLink
|
||||
to="/" flex="~" items-center
|
||||
gap-2 px-2 text-nowrap text-2xl outline-none
|
||||
>
|
||||
<template v-if="selectedOption?.kind === BackgroundKind.Wave">
|
||||
<template v-if="dark">
|
||||
<img :src="LogoDark" h-8 w-8 class="theme-colored">
|
||||
</template>
|
||||
<template v-else>
|
||||
<img :src="Logo" h-8 w-8 class="theme-colored">
|
||||
</template>
|
||||
</template>
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.theme-colored {
|
||||
filter: hue-rotate(calc(var(--chromatic-hue, 0) * 1deg));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,280 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChatHistoryItem } from '@proj-airi/stage-ui/types/chat'
|
||||
import type { ChatProvider } from '@xsai-ext/providers/utils'
|
||||
|
||||
import { ChatHistory, HearingConfigDialog } from '@proj-airi/stage-ui/components'
|
||||
import { useAudioAnalyzer } from '@proj-airi/stage-ui/composables'
|
||||
import { useAudioContext } from '@proj-airi/stage-ui/stores/audio'
|
||||
import { useChatStore } from '@proj-airi/stage-ui/stores/chat'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { useSettings, useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { BasicTextarea, useTheme } from '@proj-airi/ui'
|
||||
import { useResizeObserver, useScreenSafeArea } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import IndicatorMicVolume from '../Widgets/IndicatorMicVolume.vue'
|
||||
import ActionAbout from './InteractiveArea/Actions/About.vue'
|
||||
import ActionViewControls from './InteractiveArea/Actions/ViewControls.vue'
|
||||
import ViewControlInputs from './ViewControls/Inputs.vue'
|
||||
|
||||
import { BackgroundDialogPicker } from '../Backgrounds'
|
||||
|
||||
const { isDark, toggleDark } = useTheme()
|
||||
const hearingDialogOpen = ref(false)
|
||||
const chatStore = useChatStore()
|
||||
const { messages, sending, streamingMessage } = storeToRefs(chatStore)
|
||||
const historyMessages = computed(() => messages.value as unknown as ChatHistoryItem[])
|
||||
|
||||
const viewControlsActiveMode = ref<'x' | 'y' | 'z' | 'scale'>('scale')
|
||||
const viewControlsInputsRef = useTemplateRef<InstanceType<typeof ViewControlInputs>>('viewControlsInputs')
|
||||
|
||||
const messageInput = ref('')
|
||||
const isComposing = ref(false)
|
||||
const backgroundDialogOpen = ref(false)
|
||||
|
||||
const screenSafeArea = useScreenSafeArea()
|
||||
const providersStore = useProvidersStore()
|
||||
const { activeProvider, activeModel } = storeToRefs(useConsciousnessStore())
|
||||
|
||||
useResizeObserver(document.documentElement, () => screenSafeArea.update())
|
||||
const { themeColorsHueDynamic, stageViewControlsEnabled } = storeToRefs(useSettings())
|
||||
const settingsAudioDevice = useSettingsAudioDevice()
|
||||
const { enabled, selectedAudioInput, stream, audioInputs } = storeToRefs(settingsAudioDevice)
|
||||
const { send, onAfterMessageComposed, discoverToolsCompatibility, cleanupMessages } = chatStore
|
||||
const { t } = useI18n()
|
||||
const { audioContext } = useAudioContext()
|
||||
const { startAnalyzer, stopAnalyzer, volumeLevel } = useAudioAnalyzer()
|
||||
let analyzerSource: MediaStreamAudioSourceNode | undefined
|
||||
|
||||
function isMobileDevice() {
|
||||
return /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!isMobileDevice()) {
|
||||
await handleSend()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSend() {
|
||||
if (!messageInput.value.trim() || isComposing.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const textToSend = messageInput.value
|
||||
messageInput.value = ''
|
||||
|
||||
try {
|
||||
const providerConfig = providersStore.getProviderConfig(activeProvider.value)
|
||||
|
||||
await send(textToSend, {
|
||||
chatProvider: await providersStore.getProviderInstance(activeProvider.value) as ChatProvider,
|
||||
model: activeModel.value,
|
||||
providerConfig,
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
messageInput.value = textToSend
|
||||
messages.value.pop()
|
||||
messages.value.push({
|
||||
role: 'error',
|
||||
content: (error as Error).message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function teardownAnalyzer() {
|
||||
try {
|
||||
analyzerSource?.disconnect()
|
||||
}
|
||||
catch {}
|
||||
analyzerSource = undefined
|
||||
stopAnalyzer()
|
||||
}
|
||||
|
||||
async function setupAnalyzer() {
|
||||
teardownAnalyzer()
|
||||
if (!hearingDialogOpen.value || !enabled.value || !stream.value)
|
||||
return
|
||||
if (audioContext.state === 'suspended')
|
||||
await audioContext.resume()
|
||||
const analyser = startAnalyzer(audioContext)
|
||||
if (!analyser)
|
||||
return
|
||||
analyzerSource = audioContext.createMediaStreamSource(stream.value)
|
||||
analyzerSource.connect(analyser)
|
||||
}
|
||||
|
||||
watch([hearingDialogOpen, enabled, stream], () => {
|
||||
setupAnalyzer()
|
||||
}, { immediate: true })
|
||||
|
||||
watch(hearingDialogOpen, (value) => {
|
||||
if (value) {
|
||||
settingsAudioDevice.askPermission()
|
||||
}
|
||||
})
|
||||
|
||||
onAfterMessageComposed(async () => {
|
||||
})
|
||||
|
||||
watch([activeProvider, activeModel], async () => {
|
||||
if (activeProvider.value && activeModel.value) {
|
||||
await discoverToolsCompatibility(activeModel.value, await providersStore.getProviderInstance<ChatProvider>(activeProvider.value), [])
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
teardownAnalyzer()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
screenSafeArea.update()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div fixed bottom-0 w-full flex flex-col>
|
||||
<BackgroundDialogPicker v-model="backgroundDialogOpen" />
|
||||
<KeepAlive>
|
||||
<Transition name="fade">
|
||||
<ChatHistory
|
||||
v-if="!stageViewControlsEnabled"
|
||||
variant="mobile"
|
||||
:messages="historyMessages"
|
||||
:sending="sending"
|
||||
:streaming-message="streamingMessage"
|
||||
max-w="[calc(100%-3.5rem)]"
|
||||
w-full self-start pb-3 pl-3
|
||||
class="chat-history"
|
||||
:class="[
|
||||
'relative z-20',
|
||||
]"
|
||||
/>
|
||||
</Transition>
|
||||
</KeepAlive>
|
||||
<div relative w-full self-end>
|
||||
<div top="50%" translate-y="[-50%]" fixed z-15 px-3>
|
||||
<ViewControlInputs ref="viewControlsInputs" :mode="viewControlsActiveMode" />
|
||||
</div>
|
||||
<div translate-y="[-100%]" absolute right-0 w-full px-3 pb-3 font-sans>
|
||||
<div flex="~ col" w-full gap-1>
|
||||
<ActionAbout />
|
||||
<HearingConfigDialog
|
||||
v-model:show="hearingDialogOpen"
|
||||
v-model:enabled="enabled"
|
||||
v-model:selected-audio-input="selectedAudioInput"
|
||||
:audio-inputs="audioInputs"
|
||||
:volume-level="volumeLevel"
|
||||
:granted="true"
|
||||
>
|
||||
<button
|
||||
border="2 solid neutral-100/60 dark:neutral-800/30"
|
||||
bg="neutral-50/70 dark:neutral-800/70"
|
||||
w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md
|
||||
title="Hearing"
|
||||
>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<IndicatorMicVolume v-if="enabled" size-5 color-class="text-neutral-500 dark:text-neutral-400" />
|
||||
<div v-else i-solar:microphone-3-outline size-5 text="neutral-500 dark:neutral-400" />
|
||||
</Transition>
|
||||
</button>
|
||||
</HearingConfigDialog>
|
||||
<button border="2 solid neutral-100/60 dark:neutral-800/30" bg="neutral-50/70 dark:neutral-800/70" w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md title="Theme" @click="toggleDark()">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="isDark" i-solar:moon-outline size-5 text="neutral-500 dark:neutral-400" />
|
||||
<div v-else i-solar:sun-2-outline size-5 text="neutral-500 dark:neutral-400" />
|
||||
</Transition>
|
||||
</button>
|
||||
<button border="2 solid neutral-100/60 dark:neutral-800/30" bg="neutral-50/70 dark:neutral-800/70" w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md title="Background" @click="backgroundDialogOpen = true">
|
||||
<div i-solar:gallery-wide-bold-duotone size-5 text="neutral-500 dark:neutral-400" />
|
||||
</button>
|
||||
<!-- <button border="2 solid neutral-100/60 dark:neutral-800/30" bg="neutral-50/70 dark:neutral-800/70" w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md title="Language">
|
||||
<div i-solar:earth-outline size-5 text="neutral-500 dark:neutral-400" />
|
||||
</button> -->
|
||||
<RouterLink to="/settings" border="2 solid neutral-100/60 dark:neutral-800/30" bg="neutral-50/70 dark:neutral-800/70" w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md title="Settings">
|
||||
<div i-solar:settings-outline size-5 text="neutral-500 dark:neutral-400" />
|
||||
</RouterLink>
|
||||
<!-- <button border="2 solid neutral-100/60 dark:neutral-800/30" bg="neutral-50/70 dark:neutral-800/70" w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md title="Model">
|
||||
<div i-solar:face-scan-circle-outline size-5 text="neutral-500 dark:neutral-400" />
|
||||
</button> -->
|
||||
<ActionViewControls v-model="viewControlsActiveMode" @reset="() => viewControlsInputsRef?.resetOnMode()" />
|
||||
<button
|
||||
border="2 solid neutral-100/60 dark:neutral-800/30"
|
||||
bg="neutral-50/70 dark:neutral-800/70"
|
||||
w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md
|
||||
title="Cleanup Messages"
|
||||
@click="cleanupMessages()"
|
||||
>
|
||||
<div class="i-solar:trash-bin-2-bold-duotone" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div bg="white dark:neutral-800" max-h-100dvh max-w-100dvw w-full flex gap-1 overflow-auto px-3 pt-2 :style="{ paddingBottom: `${Math.max(Number.parseFloat(screenSafeArea.bottom.value.replace('px', '')), 12)}px` }">
|
||||
<BasicTextarea
|
||||
v-model="messageInput"
|
||||
:placeholder="t('stage.message')"
|
||||
border="solid 2 neutral-200/60 dark:neutral-700/60"
|
||||
text="neutral-500 hover:neutral-600 dark:neutral-100 dark:hover:neutral-200 placeholder:neutral-400 placeholder:hover:neutral-500 placeholder:dark:neutral-300 placeholder:dark:hover:neutral-400"
|
||||
bg="neutral-100/80 dark:neutral-950/80"
|
||||
max-h="[10lh]" min-h="[calc(1lh+4px+4px)]"
|
||||
w-full resize-none overflow-y-scroll rounded="[1lh]" px-4 py-0.5 outline-none backdrop-blur-md scrollbar-none
|
||||
transition="all duration-250 ease-in-out placeholder:all placeholder:duration-250 placeholder:ease-in-out"
|
||||
:class="[themeColorsHueDynamic ? 'transition-colors-none placeholder:transition-colors-none' : '']"
|
||||
default-height="1lh"
|
||||
@submit="handleSubmit"
|
||||
@compositionstart="isComposing = true"
|
||||
@compositionend="isComposing = false"
|
||||
/>
|
||||
<button
|
||||
v-if="messageInput.trim() || isComposing"
|
||||
w="[calc(1lh+4px+4px)]" h="[calc(1lh+4px+4px)]" aspect-square flex items-center self-end justify-center rounded-full outline-none backdrop-blur-md
|
||||
text="neutral-500 hover:neutral-600 dark:neutral-900 dark:hover:neutral-800"
|
||||
bg="primary-50/80 dark:neutral-100/80 hover:neutral-50"
|
||||
transition="all duration-250 ease-in-out"
|
||||
@click="handleSend"
|
||||
>
|
||||
<div i-solar:arrow-up-outline />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@keyframes scan {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(400%);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-scan {
|
||||
animation: scan 2s infinite linear;
|
||||
}
|
||||
|
||||
/*
|
||||
DO NOT ATTEMPT TO USE backdrop-filter TOGETHER WITH mask-image.
|
||||
|
||||
html - Why doesn't blur backdrop-filter work together with mask-image? - Stack Overflow
|
||||
https://stackoverflow.com/questions/72780266/why-doesnt-blur-backdrop-filter-work-together-with-mask-image
|
||||
*/
|
||||
.chat-history {
|
||||
--gradient: linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 20%);
|
||||
-webkit-mask-image: var(--gradient);
|
||||
mask-image: var(--gradient);
|
||||
-webkit-mask-size: 100% 100%;
|
||||
mask-size: 100% 100%;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-position: bottom;
|
||||
mask-position: bottom;
|
||||
max-height: 35dvh;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,205 @@
|
||||
<script setup lang="ts">
|
||||
import { useModelStore } from '@proj-airi/stage-ui-three'
|
||||
import { useLive2d } from '@proj-airi/stage-ui/stores/live2d'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { RoundRange } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
mode: 'x' | 'y' | 'z' | 'scale'
|
||||
}>()
|
||||
|
||||
const { stageModelRenderer, stageViewControlsEnabled } = storeToRefs(useSettings())
|
||||
const { scale: vrmScale, modelOffset: vrmPosition, modelSize: vrmModelSize } = storeToRefs(useModelStore())
|
||||
const { scale: live2dScale, position: live2dPosition } = storeToRefs(useLive2d())
|
||||
|
||||
const viewControlsValueX = computed({
|
||||
get: () => {
|
||||
switch (stageModelRenderer.value) {
|
||||
case 'live2d':
|
||||
return live2dPosition.value.x
|
||||
case 'vrm':
|
||||
return vrmPosition.value.x
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
},
|
||||
set: (value) => {
|
||||
switch (stageModelRenderer.value) {
|
||||
case 'live2d':
|
||||
live2dPosition.value.x = value
|
||||
break
|
||||
case 'vrm':
|
||||
vrmPosition.value.x = value
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const viewControlsValueXMin = computed(() => {
|
||||
return stageModelRenderer.value === 'live2d' ? -500 : -vrmModelSize.value.x - 10
|
||||
})
|
||||
|
||||
const viewControlsValueXMax = computed(() => {
|
||||
return stageModelRenderer.value === 'vrm' ? 500 : vrmModelSize.value.x + 10
|
||||
})
|
||||
|
||||
const viewControlsValueY = computed({
|
||||
get: () => {
|
||||
switch (stageModelRenderer.value) {
|
||||
case 'live2d':
|
||||
return live2dPosition.value.y
|
||||
case 'vrm':
|
||||
return vrmPosition.value.y
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
},
|
||||
set: (value) => {
|
||||
switch (stageModelRenderer.value) {
|
||||
case 'live2d':
|
||||
live2dPosition.value.y = value
|
||||
break
|
||||
case 'vrm':
|
||||
vrmPosition.value.y = value
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const viewControlsValueYMin = computed(() => {
|
||||
return stageModelRenderer.value === 'live2d' ? -500 : -vrmModelSize.value.y - 10
|
||||
})
|
||||
|
||||
const viewControlsValueYMax = computed(() => {
|
||||
return stageModelRenderer.value === 'vrm' ? 500 : vrmModelSize.value.y + 10
|
||||
})
|
||||
|
||||
const viewControlsValueZ = computed({
|
||||
get: () => {
|
||||
switch (stageModelRenderer.value) {
|
||||
case 'live2d':
|
||||
return 0
|
||||
case 'vrm':
|
||||
return vrmPosition.value.z
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
},
|
||||
set: (value) => {
|
||||
switch (stageModelRenderer.value) {
|
||||
case 'live2d':
|
||||
break
|
||||
case 'vrm':
|
||||
vrmPosition.value.z = value
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const viewControlsValueZMin = computed(() => {
|
||||
return stageModelRenderer.value === 'live2d' ? -500 : -vrmModelSize.value.z - 10
|
||||
})
|
||||
|
||||
const viewControlsValueZMax = computed(() => {
|
||||
return stageModelRenderer.value === 'live2d' ? 500 : vrmModelSize.value.z + 10
|
||||
})
|
||||
|
||||
const viewControlsValueScale = computed({
|
||||
get: () => {
|
||||
if (stageModelRenderer.value === 'live2d') {
|
||||
return live2dScale.value
|
||||
}
|
||||
|
||||
return vrmScale.value
|
||||
},
|
||||
set: (value) => {
|
||||
if (stageModelRenderer.value === 'live2d') {
|
||||
live2dScale.value = value
|
||||
}
|
||||
else {
|
||||
vrmScale.value = value
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function resetOnMode() {
|
||||
switch (props.mode) {
|
||||
case 'x':
|
||||
viewControlsValueX.value = 0
|
||||
break
|
||||
case 'y':
|
||||
viewControlsValueY.value = 0
|
||||
break
|
||||
case 'z':
|
||||
viewControlsValueZ.value = 0
|
||||
break
|
||||
case 'scale':
|
||||
viewControlsValueScale.value = 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
resetOnMode,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="fade-side-pops-in">
|
||||
<div v-if="stageViewControlsEnabled">
|
||||
<Transition name="fade-side-pops-in" mode="out-in">
|
||||
<div v-if="props.mode === 'x'" relative class="[&_.round-range-tooltip]:hover:opacity-100">
|
||||
<RoundRange v-model="viewControlsValueX" :min="viewControlsValueXMin" :max="viewControlsValueXMax" :step="0.01" data-direction="vertical" h="50%" write-vertical-left />
|
||||
<div class="round-range-tooltip" top="50%" translate-y="[-50%]" absolute left-10 font-mono op-0 transition="all duration-200 ease-in-out">
|
||||
{{ viewControlsValueX.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="props.mode === 'y'" relative class="[&_.round-range-tooltip]:hover:opacity-100">
|
||||
<RoundRange v-model="viewControlsValueY" :min="viewControlsValueYMin" :max="viewControlsValueYMax" :step="0.01" write-vertical-left h="50%" data-direction="vertical" />
|
||||
<div class="round-range-tooltip" top="50%" translate-y="[-50%]" absolute left-10 font-mono op-0 transition="all duration-200 ease-in-out">
|
||||
{{ viewControlsValueY.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="stageModelRenderer === 'vrm' && props.mode === 'z'" relative class="[&_.round-range-tooltip]:hover:opacity-100">
|
||||
<RoundRange v-model="viewControlsValueZ" :min="viewControlsValueZMin" :max="viewControlsValueZMax" :step="0.01" write-vertical-left h="50%" data-direction="vertical" />
|
||||
<div class="round-range-tooltip" top="50%" translate-y="[-50%]" absolute left-10 font-mono op-0 transition="all duration-200 ease-in-out">
|
||||
{{ viewControlsValueZ.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="props.mode === 'scale'" relative class="[&_.round-range-tooltip]:hover:opacity-100">
|
||||
<RoundRange v-model="viewControlsValueScale" :min="0" :max="3" :step="0.0001" write-vertical-left h="50%" data-direction="vertical" />
|
||||
<div class="round-range-tooltip" top="50%" translate-y="[-50%]" absolute left-10 font-mono op-0 transition="all duration-200 ease-in-out">
|
||||
{{ viewControlsValueScale.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-side-pops-in-enter-active,
|
||||
.fade-side-pops-in-leave-active {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-side-pops-in-enter-from,
|
||||
.fade-side-pops-in-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-100%) scale(0.8);
|
||||
}
|
||||
|
||||
.fade-side-pops-in-enter-to,
|
||||
.fade-side-pops-in-leave-from {
|
||||
opacity: 1;
|
||||
transform: translateX(0) scale(1);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import { useChatStore } from '@proj-airi/stage-ui/stores/chat'
|
||||
import { useTheme } from '@proj-airi/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { BackgroundDialogPicker } from '../Backgrounds'
|
||||
|
||||
const { cleanupMessages } = useChatStore()
|
||||
const { isDark, toggleDark } = useTheme()
|
||||
|
||||
const backgroundDialogOpen = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BackgroundDialogPicker v-model="backgroundDialogOpen" />
|
||||
<div absolute bottom--8 right-0 flex gap-2>
|
||||
<button
|
||||
class="max-h-[10lh] min-h-[1lh]"
|
||||
bg="neutral-100 dark:neutral-800"
|
||||
text="lg neutral-500 dark:neutral-400"
|
||||
hover:text="red-500 dark:red-400"
|
||||
flex items-center justify-center rounded-md p-2 outline-none
|
||||
transition-colors transition-transform active:scale-95
|
||||
@click="cleanupMessages()"
|
||||
>
|
||||
<div class="i-solar:trash-bin-2-bold-duotone" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="max-h-[10lh] min-h-[1lh]"
|
||||
bg="neutral-100 dark:neutral-800"
|
||||
text="lg neutral-500 dark:neutral-400"
|
||||
flex items-center justify-center rounded-md p-2 outline-none
|
||||
transition-colors transition-transform active:scale-95
|
||||
@click="() => toggleDark()"
|
||||
>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="isDark" i-solar:moon-bold />
|
||||
<div v-else i-solar:sun-2-bold />
|
||||
</Transition>
|
||||
</button>
|
||||
<button
|
||||
class="max-h-[10lh] min-h-[1lh]"
|
||||
bg="neutral-100 dark:neutral-800"
|
||||
text="lg neutral-500 dark:neutral-400"
|
||||
flex items-center justify-center rounded-md p-2 outline-none
|
||||
transition-colors transition-transform active:scale-95
|
||||
title="Background"
|
||||
@click="backgroundDialogOpen = true"
|
||||
>
|
||||
<div i-solar:gallery-wide-bold-duotone />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,210 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChatProvider } from '@xsai-ext/providers/utils'
|
||||
|
||||
import { useAudioAnalyzer } from '@proj-airi/stage-ui/composables'
|
||||
import { useAudioContext } from '@proj-airi/stage-ui/stores/audio'
|
||||
import { useChatStore } from '@proj-airi/stage-ui/stores/chat'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { useSettings, useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { BasicTextarea, FieldSelect } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from 'reka-ui'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IndicatorMicVolume from './IndicatorMicVolume.vue'
|
||||
|
||||
const messageInput = ref('')
|
||||
const hearingTooltipOpen = ref(false)
|
||||
const isComposing = ref(false)
|
||||
|
||||
const providersStore = useProvidersStore()
|
||||
const { activeProvider, activeModel } = storeToRefs(useConsciousnessStore())
|
||||
const { themeColorsHueDynamic } = storeToRefs(useSettings())
|
||||
|
||||
const { askPermission } = useSettingsAudioDevice()
|
||||
const { enabled, selectedAudioInput, stream, audioInputs } = storeToRefs(useSettingsAudioDevice())
|
||||
const chatStore = useChatStore()
|
||||
const { send, onAfterMessageComposed, discoverToolsCompatibility } = chatStore
|
||||
const { messages } = storeToRefs(useChatStore())
|
||||
const { audioContext } = useAudioContext()
|
||||
const { t } = useI18n()
|
||||
|
||||
async function handleSend() {
|
||||
if (!messageInput.value.trim() || isComposing.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const textToSend = messageInput.value
|
||||
messageInput.value = ''
|
||||
|
||||
try {
|
||||
const providerConfig = providersStore.getProviderConfig(activeProvider.value)
|
||||
|
||||
await send(textToSend, {
|
||||
chatProvider: await providersStore.getProviderInstance(activeProvider.value) as ChatProvider,
|
||||
model: activeModel.value,
|
||||
providerConfig,
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
messageInput.value = textToSend
|
||||
messages.value.pop()
|
||||
messages.value.push({
|
||||
role: 'error',
|
||||
content: (error as Error).message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
watch(hearingTooltipOpen, async (value) => {
|
||||
if (value) {
|
||||
await askPermission()
|
||||
}
|
||||
})
|
||||
|
||||
watch([activeProvider, activeModel], async () => {
|
||||
if (activeProvider.value && activeModel.value) {
|
||||
await discoverToolsCompatibility(activeModel.value, await providersStore.getProviderInstance<ChatProvider>(activeProvider.value), [])
|
||||
}
|
||||
})
|
||||
|
||||
onAfterMessageComposed(async () => {
|
||||
})
|
||||
|
||||
const { startAnalyzer, stopAnalyzer, volumeLevel } = useAudioAnalyzer()
|
||||
const normalizedVolume = computed(() => Math.min(1, Math.max(0, (volumeLevel.value ?? 0) / 100)))
|
||||
let analyzerSource: MediaStreamAudioSourceNode | undefined
|
||||
|
||||
function teardownAnalyzer() {
|
||||
try {
|
||||
analyzerSource?.disconnect()
|
||||
}
|
||||
catch {}
|
||||
analyzerSource = undefined
|
||||
stopAnalyzer()
|
||||
}
|
||||
|
||||
async function setupAnalyzer() {
|
||||
teardownAnalyzer()
|
||||
if (!hearingTooltipOpen.value || !enabled.value || !stream.value)
|
||||
return
|
||||
if (audioContext.state === 'suspended')
|
||||
await audioContext.resume()
|
||||
const analyser = startAnalyzer(audioContext)
|
||||
if (!analyser)
|
||||
return
|
||||
analyzerSource = audioContext.createMediaStreamSource(stream.value)
|
||||
analyzerSource.connect(analyser)
|
||||
}
|
||||
|
||||
watch([hearingTooltipOpen, enabled, stream], () => {
|
||||
setupAnalyzer()
|
||||
}, { immediate: true })
|
||||
|
||||
onUnmounted(() => {
|
||||
teardownAnalyzer()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div h="<md:full" flex gap-2 class="ph-no-capture">
|
||||
<div
|
||||
:class="[
|
||||
'relative',
|
||||
'w-full',
|
||||
'bg-primary-200/20 dark:bg-primary-400/20',
|
||||
]"
|
||||
>
|
||||
<BasicTextarea
|
||||
v-model="messageInput"
|
||||
:placeholder="t('stage.message')"
|
||||
text="primary-600 dark:primary-100 placeholder:primary-500 dark:placeholder:primary-200"
|
||||
bg="transparent"
|
||||
min-h="[100px]" max-h="[300px]" w-full
|
||||
rounded-t-xl p-4 font-medium
|
||||
outline-none transition="all duration-250 ease-in-out placeholder:all placeholder:duration-250 placeholder:ease-in-out"
|
||||
:class="{
|
||||
'transition-colors-none placeholder:transition-colors-none': themeColorsHueDynamic,
|
||||
}"
|
||||
@submit="handleSend"
|
||||
@compositionstart="isComposing = true"
|
||||
@compositionend="isComposing = false"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<TooltipProvider :delay-duration="0" :skip-delay-duration="0">
|
||||
<TooltipRoot v-model:open="hearingTooltipOpen">
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
class="max-h-[10lh] min-h-[1lh]"
|
||||
text="lg neutral-500 dark:neutral-400"
|
||||
flex items-center justify-center rounded-md p-2 outline-none
|
||||
transition="colors duration-200, transform duration-100" active:scale-95
|
||||
:title="t('settings.hearing.title')"
|
||||
>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<IndicatorMicVolume v-if="enabled" />
|
||||
<div v-else class="i-ph:microphone-slash" />
|
||||
</Transition>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<Transition name="fade">
|
||||
<TooltipContent
|
||||
side="top"
|
||||
:side-offset="8"
|
||||
:class="[
|
||||
'w-72 max-w-[18rem] rounded-xl border border-neutral-200/60 bg-neutral-50/90 p-4',
|
||||
'shadow-lg backdrop-blur-md dark:border-neutral-800/30 dark:bg-neutral-900/80',
|
||||
'flex flex-col gap-3',
|
||||
]"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="relative h-28 w-28 select-none">
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-20 w-20 rounded-full transition-all duration-150 -translate-x-1/2 -translate-y-1/2"
|
||||
:style="{ transform: `translate(-50%, -50%) scale(${1 + normalizedVolume * 0.35})`, opacity: String(0.25 + normalizedVolume * 0.25) }"
|
||||
:class="enabled ? 'bg-primary-500/15 dark:bg-primary-600/20' : 'bg-neutral-300/20 dark:bg-neutral-700/20'"
|
||||
/>
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-24 w-24 rounded-full transition-all duration-200 -translate-x-1/2 -translate-y-1/2"
|
||||
:style="{ transform: `translate(-50%, -50%) scale(${1.2 + normalizedVolume * 0.55})`, opacity: String(0.15 + normalizedVolume * 0.2) }"
|
||||
:class="enabled ? 'bg-primary-500/10 dark:bg-primary-600/15' : 'bg-neutral-300/10 dark:bg-neutral-700/10'"
|
||||
/>
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-28 w-28 rounded-full transition-all duration-300 -translate-x-1/2 -translate-y-1/2"
|
||||
:style="{ transform: `translate(-50%, -50%) scale(${1.5 + normalizedVolume * 0.8})`, opacity: String(0.08 + normalizedVolume * 0.15) }"
|
||||
:class="enabled ? 'bg-primary-500/5 dark:bg-primary-600/10' : 'bg-neutral-300/5 dark:bg-neutral-700/5'"
|
||||
/>
|
||||
<button
|
||||
class="absolute left-1/2 top-1/2 grid h-16 w-16 place-items-center rounded-full shadow-md outline-none transition-all duration-200 -translate-x-1/2 -translate-y-1/2"
|
||||
:class="enabled
|
||||
? 'bg-primary-500 text-white hover:bg-primary-600 active:scale-95'
|
||||
: 'bg-neutral-200 text-neutral-600 hover:bg-neutral-300 active:scale-95 dark:bg-neutral-700 dark:text-neutral-200'"
|
||||
@click="enabled = !enabled"
|
||||
>
|
||||
<div :class="enabled ? 'i-ph:microphone' : 'i-ph:microphone-slash'" class="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-3 text-xs text-neutral-500 dark:text-neutral-400">
|
||||
{{ enabled ? 'Microphone enabled' : 'Microphone disabled' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FieldSelect
|
||||
v-model="selectedAudioInput"
|
||||
label="Input device"
|
||||
description="Select the microphone you want to use."
|
||||
:options="audioInputs.map(device => ({ label: device.label || 'Unknown Device', value: device.deviceId }))"
|
||||
layout="vertical"
|
||||
placeholder="Select microphone"
|
||||
/>
|
||||
</TooltipContent>
|
||||
</Transition>
|
||||
</TooltipRoot>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div
|
||||
flex="~ col"
|
||||
border="solid 4 primary-200/20 dark:primary-400/20"
|
||||
h-full w-full rounded-xl
|
||||
bg="primary-50/50 dark:primary-950/70" backdrop-blur-md
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
import { useAudioAnalyzer } from '@proj-airi/stage-ui/composables'
|
||||
import { useAudioContext } from '@proj-airi/stage-ui/stores/audio'
|
||||
import { useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{ colorClass?: string }>(), { colorClass: 'text-primary-500 dark:text-primary-200' })
|
||||
const settingsAudio = useSettingsAudioDevice()
|
||||
const { stream, enabled } = storeToRefs(settingsAudio)
|
||||
|
||||
const { audioContext } = storeToRefs(useAudioContext())
|
||||
const { startAnalyzer, stopAnalyzer, volumeLevel } = useAudioAnalyzer()
|
||||
|
||||
let source: MediaStreamAudioSourceNode | undefined
|
||||
|
||||
const normalized = computed(() => Math.min(1, (volumeLevel.value ?? 0) / 100))
|
||||
|
||||
function teardown() {
|
||||
try {
|
||||
source?.disconnect()
|
||||
}
|
||||
catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
source = undefined
|
||||
stopAnalyzer()
|
||||
}
|
||||
|
||||
async function setup() {
|
||||
teardown()
|
||||
if (!enabled.value || !stream.value)
|
||||
return
|
||||
|
||||
const ctx = audioContext.value
|
||||
if (ctx.state === 'suspended')
|
||||
await ctx.resume()
|
||||
const analyser = startAnalyzer(ctx)
|
||||
if (!analyser)
|
||||
return
|
||||
source = ctx.createMediaStreamSource(stream.value)
|
||||
source.connect(analyser)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
watch([enabled, stream], () => setup(), { immediate: true })
|
||||
})
|
||||
|
||||
onUnmounted(() => teardown())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['flex items-center justify-center', props.colorClass]">
|
||||
<svg width="24" height="24" viewBox="0 0 256 256" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="micLevel" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="currentColor" stop-opacity="0" />
|
||||
<stop :offset="`${100 - Math.round(normalized * 100)}%`" stop-color="currentColor" stop-opacity="0" />
|
||||
<stop :offset="`${100 - Math.round(normalized * 100)}%`" stop-color="currentColor" stop-opacity="0.95" />
|
||||
<stop offset="100%" stop-color="currentColor" stop-opacity="0.95" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
fill="url(#micLevel)"
|
||||
d="M128 176a48.05 48.05 0 0 0 48-48V64a48 48 0 0 0-96 0v64a48.05 48.05 0 0 0 48 48M96 64a32 32 0 0 1 64 0v64a32 32 0 0 1-64 0Zm40 143.6V240a8 8 0 0 1-16 0v-32.4A80.11 80.11 0 0 1 48 128a8 8 0 0 1 16 0a64 64 0 0 0 128 0a8 8 0 0 1 16 0a80.11 80.11 0 0 1-72 79.6"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-opacity="1"
|
||||
stroke-width="2"
|
||||
d="M128 176a48.05 48.05 0 0 0 48-48V64a48 48 0 0 0-96 0v64a48.05 48.05 0 0 0 48 48M96 64a32 32 0 0 1 64 0v64a32 32 0 0 1-64 0Zm40 143.6V240a8 8 0 0 1-16 0v-32.4A80.11 80.11 0 0 1 48 128a8 8 0 0 1 16 0a64 64 0 0 0 128 0a8 8 0 0 1 16 0a80.11 80.11 0 0 1-72 79.6"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useDevicesList, useUserMedia } from '@vueuse/core'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
export function useAudioInput() {
|
||||
const devices = useDevicesList({ constraints: { audio: true }, requestPermissions: false })
|
||||
|
||||
const selectedAudioInputId = ref<string>(devices.audioInputs.value[0]?.deviceId || '')
|
||||
const selectedAudioInput = ref<MediaDeviceInfo>()
|
||||
const audioInputs = computed(() => devices.audioInputs.value)
|
||||
|
||||
const constraints = ref<MediaStreamConstraints>({ audio: true })
|
||||
const media = useUserMedia({ constraints, autoSwitch: true, enabled: false })
|
||||
|
||||
async function request() {
|
||||
if (devices.permissionGranted.value) {
|
||||
return
|
||||
}
|
||||
if (!devices.isSupported.value) {
|
||||
return
|
||||
}
|
||||
|
||||
await devices.ensurePermissions()
|
||||
}
|
||||
|
||||
watch(selectedAudioInputId, () => {
|
||||
if (selectedAudioInputId.value) {
|
||||
constraints.value = {
|
||||
audio: {
|
||||
deviceId: { exact: selectedAudioInputId.value! },
|
||||
},
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
watch(devices.audioInputs, () => {
|
||||
selectedAudioInput.value = audioInputs.value.find(device => device.deviceId === selectedAudioInputId.value)
|
||||
}, { immediate: true })
|
||||
|
||||
watch([devices.permissionGranted, audioInputs, selectedAudioInputId], async () => {
|
||||
await request()
|
||||
if (!devices.permissionGranted.value) {
|
||||
return
|
||||
}
|
||||
if (audioInputs.value.length === 0) {
|
||||
return
|
||||
}
|
||||
if (!selectedAudioInput.value) {
|
||||
selectedAudioInput.value = audioInputs.value[0]
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
async function start() {
|
||||
await request()
|
||||
|
||||
if (!devices.permissionGranted.value) {
|
||||
return
|
||||
}
|
||||
if (!selectedAudioInput.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (media.enabled.value) {
|
||||
media.restart()
|
||||
}
|
||||
|
||||
media.start()
|
||||
}
|
||||
|
||||
function stop() {
|
||||
media.stop()
|
||||
}
|
||||
|
||||
return {
|
||||
selectedAudioInputId,
|
||||
selectedAudioInput,
|
||||
audioInputs,
|
||||
|
||||
start,
|
||||
stop,
|
||||
request,
|
||||
media,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
|
||||
import { until } from '@vueuse/core'
|
||||
import { ref, toRef } from 'vue'
|
||||
|
||||
export function useAudioRecord(
|
||||
media: MaybeRefOrGetter<MediaStream | undefined>,
|
||||
start: () => Promise<void> = () => Promise.resolve(),
|
||||
) {
|
||||
const audioRecorder = ref<MediaRecorder>()
|
||||
const mediaRef = toRef(media)
|
||||
|
||||
async function startRecord() {
|
||||
await start()
|
||||
await until(mediaRef).toBeTruthy()
|
||||
|
||||
if (!mediaRef.value) {
|
||||
console.error('No media media available')
|
||||
return
|
||||
}
|
||||
|
||||
audioRecorder.value = new MediaRecorder(mediaRef.value)
|
||||
audioRecorder.value.start()
|
||||
}
|
||||
|
||||
function stopRecord() {
|
||||
if (audioRecorder.value) {
|
||||
audioRecorder.value.stop()
|
||||
audioRecorder.value.ondataavailable = (event) => {
|
||||
const audioBlob = event.data
|
||||
const audioUrl = URL.createObjectURL(audioBlob)
|
||||
const audioElement = new Audio(audioUrl)
|
||||
audioElement.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
startRecord,
|
||||
stopRecord,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
export function useIconAnimation(icon: string) {
|
||||
const iconAnimationStarted = ref(false)
|
||||
const showAnimationComponent = ref(false)
|
||||
const animationIcon = ref(icon)
|
||||
|
||||
const settingsStore = useSettings()
|
||||
const showIconAnimation = computed(() => showAnimationComponent.value && !settingsStore.disableTransitions && settingsStore.usePageSpecificTransitions)
|
||||
|
||||
onMounted(() => {
|
||||
showAnimationComponent.value = true
|
||||
requestAnimationFrame(() => {
|
||||
iconAnimationStarted.value = true
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
iconAnimationStarted.value = false
|
||||
showAnimationComponent.value = false
|
||||
})
|
||||
|
||||
return {
|
||||
iconAnimationStarted,
|
||||
showIconAnimation,
|
||||
animationIcon,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import type { BackgroundProvider } from '../components/Backgrounds'
|
||||
import type { BackgroundItem } from '../stores/background'
|
||||
|
||||
import Color from 'colorjs.io'
|
||||
|
||||
import { withRetry } from '@moeru/std'
|
||||
import { colorFromElement, patchThemeSamplingHtml2CanvasClone } from '@proj-airi/stage-ui/libs'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { useTheme } from '@proj-airi/ui'
|
||||
import { useDocumentVisibility, useIntervalFn } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { nextTick, watch } from 'vue'
|
||||
|
||||
import { BackgroundKind } from '../stores/background'
|
||||
|
||||
export function themeColorFromPropertyOf(colorFromClass: string, property: string): () => Promise<string> {
|
||||
return async () => {
|
||||
const fetchUntilWidgetMounted = withRetry(() => {
|
||||
const widgets = document.querySelector(colorFromClass) as HTMLDivElement | undefined
|
||||
if (!widgets)
|
||||
throw new Error('Widgets element not found')
|
||||
|
||||
return widgets
|
||||
}, { retry: 10, retryDelay: 1000 })
|
||||
|
||||
const widgets = await fetchUntilWidgetMounted()
|
||||
return window.getComputedStyle(widgets).getPropertyValue(property)
|
||||
}
|
||||
}
|
||||
|
||||
export function themeColorFromValue(value: string | { light: string, dark: string }): () => Promise<string> {
|
||||
return async () => {
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
else {
|
||||
const { isDark: dark } = useTheme()
|
||||
return dark.value ? value.dark : value.light
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useThemeColor(colorFrom: () => string | Promise<string>) {
|
||||
async function updateThemeColor() {
|
||||
if (!('document' in globalThis) || globalThis.document == null)
|
||||
return
|
||||
if (!('window' in globalThis) || globalThis.window == null)
|
||||
return
|
||||
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', new Color(await colorFrom()).to('srgb').toString({ format: 'hex' }))
|
||||
}
|
||||
|
||||
return {
|
||||
updateThemeColor,
|
||||
}
|
||||
}
|
||||
|
||||
export function useBackgroundThemeColor({
|
||||
backgroundSurface,
|
||||
selectedOption,
|
||||
sampledColor,
|
||||
}: {
|
||||
backgroundSurface: Ref<InstanceType<typeof BackgroundProvider> | undefined | null>
|
||||
selectedOption: Ref<BackgroundItem | undefined>
|
||||
sampledColor: Ref<string>
|
||||
}) {
|
||||
const visibility = useDocumentVisibility()
|
||||
const { themeColorsHueDynamic } = storeToRefs(useSettings())
|
||||
|
||||
let samplingToken = 0
|
||||
|
||||
const { isDark } = useTheme()
|
||||
const waveThemeColor = themeColorFromPropertyOf('.widgets.top-widgets .colored-area', 'background-color')
|
||||
|
||||
const { updateThemeColor } = useThemeColor(() => {
|
||||
if (selectedOption.value?.kind === BackgroundKind.Wave) {
|
||||
return waveThemeColor()
|
||||
}
|
||||
|
||||
return sampledColor.value
|
||||
})
|
||||
|
||||
// Keep theme-color reasonably fresh for animated wave backgrounds without doing per-frame work.
|
||||
const { pause, resume } = useIntervalFn(() => {
|
||||
if (visibility.value !== 'visible')
|
||||
return
|
||||
if (selectedOption.value?.kind === BackgroundKind.Wave && themeColorsHueDynamic)
|
||||
void updateThemeColor()
|
||||
}, 250, { immediate: false })
|
||||
|
||||
watch([() => selectedOption.value?.kind, () => themeColorsHueDynamic], ([kind, dynamic]) => {
|
||||
if (kind === BackgroundKind.Wave && dynamic) {
|
||||
void updateThemeColor()
|
||||
resume()
|
||||
}
|
||||
else {
|
||||
pause()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
async function waitForBackgroundReady() {
|
||||
await nextTick()
|
||||
const image = backgroundSurface.value?.surfaceEl?.querySelector('img')
|
||||
if (image && !image.complete) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
image.addEventListener('load', () => resolve(), { once: true })
|
||||
image.addEventListener('error', () => reject(new Error('Background image failed to load')), { once: true })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Exposed for optional manual triggers; also used within syncBackgroundTheme.
|
||||
async function sampleBackgroundColor() {
|
||||
const token = ++samplingToken
|
||||
const optionId = selectedOption.value?.id
|
||||
if (selectedOption.value?.kind === BackgroundKind.Wave) {
|
||||
await updateThemeColor()
|
||||
return
|
||||
}
|
||||
|
||||
const el = backgroundSurface.value?.surfaceEl
|
||||
if (!el)
|
||||
return
|
||||
|
||||
await waitForBackgroundReady()
|
||||
|
||||
const result = await colorFromElement(el, {
|
||||
mode: 'html2canvas',
|
||||
html2canvas: {
|
||||
region: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: el.offsetWidth,
|
||||
height: Math.min(140, el.offsetHeight),
|
||||
},
|
||||
sampleHeight: 20,
|
||||
sampleStride: 10,
|
||||
scale: 0.5,
|
||||
backgroundColor: null,
|
||||
allowTaint: true,
|
||||
useCORS: true,
|
||||
onclone: patchThemeSamplingHtml2CanvasClone,
|
||||
},
|
||||
})
|
||||
|
||||
const color = result.html2canvas?.average
|
||||
if (token !== samplingToken)
|
||||
return
|
||||
if (optionId && selectedOption.value?.id !== optionId)
|
||||
return
|
||||
|
||||
if (color) {
|
||||
sampledColor.value = color
|
||||
}
|
||||
}
|
||||
|
||||
async function syncBackgroundTheme() {
|
||||
if (selectedOption.value?.kind === BackgroundKind.Wave) {
|
||||
await updateThemeColor()
|
||||
}
|
||||
else if (sampledColor.value) {
|
||||
await updateThemeColor()
|
||||
}
|
||||
else {
|
||||
await sampleBackgroundColor()
|
||||
}
|
||||
}
|
||||
|
||||
watch([selectedOption], () => {
|
||||
syncBackgroundTheme()
|
||||
}, { immediate: true })
|
||||
|
||||
watch(sampledColor, () => {
|
||||
syncBackgroundTheme()
|
||||
})
|
||||
|
||||
watch(() => backgroundSurface.value?.surfaceEl, (el) => {
|
||||
if (el)
|
||||
syncBackgroundTheme()
|
||||
})
|
||||
|
||||
watch(isDark, () => {
|
||||
syncBackgroundTheme()
|
||||
})
|
||||
|
||||
return {
|
||||
sampledColor,
|
||||
sampleBackgroundColor,
|
||||
syncBackgroundTheme,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
import HeaderLink from '../components/Layouts/HeaderLink.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:style="{
|
||||
paddingBottom: 'env(safe-area-inset-bottom, 0px)',
|
||||
paddingTop: 'env(safe-area-inset-top, 0px)',
|
||||
paddingRight: 'env(safe-area-inset-right, 0px)',
|
||||
paddingLeft: 'env(safe-area-inset-left, 0px)',
|
||||
}"
|
||||
h-full
|
||||
>
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="px-0 py-1 md:px-3 md:py-3"
|
||||
w-full gap-2
|
||||
bg="$bg-color"
|
||||
>
|
||||
<HeaderLink />
|
||||
</div>
|
||||
<!-- Content -->
|
||||
<div class="px-3 py-2 md:px-5 md:py-5" flex="~ col" h="[calc(100%-56px)]">
|
||||
<RouterView />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:style="{
|
||||
paddingBottom: 'env(safe-area-inset-bottom, 0px)',
|
||||
paddingTop: 'env(safe-area-inset-top, 0px)',
|
||||
paddingRight: 'env(safe-area-inset-right, 0px)',
|
||||
paddingLeft: 'env(safe-area-inset-left, 0px)',
|
||||
}"
|
||||
h-full
|
||||
>
|
||||
<!-- Content -->
|
||||
<div class="px-3 py-2 md:px-5 md:py-5" flex="~ col" h-full>
|
||||
<RouterView />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
@@ -0,0 +1,157 @@
|
||||
<script setup lang="ts">
|
||||
import { PageHeader } from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { useTheme } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RouterView, useRoute } from 'vue-router'
|
||||
|
||||
import HeaderLink from '../components/Layouts/HeaderLink.vue'
|
||||
|
||||
import { themeColorFromValue, useThemeColor } from '../composables/theme-color'
|
||||
|
||||
const route = useRoute()
|
||||
const { isDark: dark } = useTheme()
|
||||
const { t } = useI18n()
|
||||
const providersStore = useProvidersStore()
|
||||
const { allProvidersMetadata } = storeToRefs(providersStore)
|
||||
|
||||
const routeHeaderMetadataMap = computed(() => {
|
||||
const map: Record<string, { subtitle?: string, title: string }> = {
|
||||
'/settings/airi-card': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.card.title'),
|
||||
},
|
||||
'/settings/system': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.system.title'),
|
||||
},
|
||||
'/settings/system/general': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.system.general.title'),
|
||||
},
|
||||
'/settings/system/color-scheme': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.system.color-scheme.title'),
|
||||
},
|
||||
'/settings/system/developer': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.system.developer.title'),
|
||||
},
|
||||
'/settings/memory': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.memory.title'),
|
||||
},
|
||||
'/settings/models': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.models.title'),
|
||||
},
|
||||
'/settings/modules': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.modules.title'),
|
||||
},
|
||||
'/settings/modules/consciousness': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.modules.consciousness.title'),
|
||||
},
|
||||
'/settings/modules/speech': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.modules.speech.title'),
|
||||
},
|
||||
'/settings/modules/hearing': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.modules.hearing.title'),
|
||||
},
|
||||
'/settings/modules/vision': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.modules.vision.title'),
|
||||
},
|
||||
'/settings/modules/memory-short-term': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.modules.memory-short-term.title'),
|
||||
},
|
||||
'/settings/modules/memory-long-term': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.modules.memory-long-term.title'),
|
||||
},
|
||||
'/settings/modules/messaging-discord': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.modules.messaging-discord.title'),
|
||||
},
|
||||
'/settings/modules/x': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.modules.x.title'),
|
||||
},
|
||||
'/settings/modules/gaming-minecraft': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.modules.gaming-minecraft.title'),
|
||||
},
|
||||
'/settings/modules/gaming-factorio': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.modules.gaming-factorio.title'),
|
||||
},
|
||||
'/settings/providers': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.providers.title'),
|
||||
},
|
||||
'/settings/data': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.data.title'),
|
||||
},
|
||||
'/settings/scene': {
|
||||
subtitle: t('settings.title'),
|
||||
title: t('settings.pages.scene.title'),
|
||||
},
|
||||
'/settings': {
|
||||
title: t('settings.title'),
|
||||
},
|
||||
}
|
||||
|
||||
for (const metadata of allProvidersMetadata.value) {
|
||||
map[`/settings/providers/${metadata.category}/${metadata.id}`] = {
|
||||
subtitle: t('settings.title'),
|
||||
title: t(metadata.nameKey),
|
||||
}
|
||||
}
|
||||
|
||||
return map
|
||||
})
|
||||
|
||||
// const activeSettingsTutorial = ref('default')
|
||||
const routeHeaderMetadata = computed(() => routeHeaderMetadataMap.value[route.path])
|
||||
|
||||
const { updateThemeColor } = useThemeColor(themeColorFromValue({ light: 'rgb(255 255 255)', dark: 'rgb(18 18 18)' }))
|
||||
watch(dark, () => updateThemeColor(), { immediate: true })
|
||||
watch(route, () => updateThemeColor(), { immediate: true })
|
||||
onMounted(() => updateThemeColor())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:style="{
|
||||
paddingBottom: 'env(safe-area-inset-bottom, 0px)',
|
||||
paddingTop: 'env(safe-area-inset-top, 0px)',
|
||||
paddingRight: 'env(safe-area-inset-right, 0px)',
|
||||
paddingLeft: 'env(safe-area-inset-left, 0px)',
|
||||
}"
|
||||
h-full w-full
|
||||
>
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="px-0 py-1 hidden sm:block md:px-3 md:py-3"
|
||||
w-full gap-2
|
||||
bg="$bg-color"
|
||||
>
|
||||
<HeaderLink />
|
||||
</div>
|
||||
<!-- Content -->
|
||||
<div class="max-h-[calc(100%-40px)] px-3 py-0 sm:max-h-[calc(100%-56px)] 2xl:max-w-screen-2xl md:py-0 xl:px-4" flex="~ col" mx-auto h-full>
|
||||
<PageHeader
|
||||
:title="routeHeaderMetadata?.title"
|
||||
:subtitle="routeHeaderMetadata?.subtitle"
|
||||
/>
|
||||
<RouterView />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main text="gray-700 dark:gray-200" h-full font-cute>
|
||||
<RouterView />
|
||||
</main>
|
||||
</template>
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { Plugin } from 'vue'
|
||||
import type { Router, RouteRecordRaw } from 'vue-router'
|
||||
|
||||
import Tres from '@tresjs/core'
|
||||
import NProgress from 'nprogress'
|
||||
|
||||
import { autoAnimatePlugin } from '@formkit/auto-animate/vue'
|
||||
import { MotionPlugin } from '@vueuse/motion'
|
||||
import { createPinia } from 'pinia'
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
|
||||
import { routes } from 'vue-router/auto-routes'
|
||||
|
||||
import App from './App.vue'
|
||||
|
||||
import { i18n } from './modules/i18n'
|
||||
|
||||
import './modules/posthog'
|
||||
|
||||
// eslint-disable-next-line perfectionist/sort-imports
|
||||
import '@proj-airi/font-cjkfonts-allseto/index.css'
|
||||
import '@proj-airi/font-xiaolai/index.css'
|
||||
import '@unocss/reset/tailwind.css'
|
||||
import './styles/main.css'
|
||||
import 'uno.css'
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
// TODO: vite-plugin-vue-layouts is long deprecated, replace with another layout solution
|
||||
const routeRecords = setupLayouts(routes as RouteRecordRaw[])
|
||||
|
||||
let router: Router
|
||||
if (import.meta.env.VITE_APP_TARGET_HUGGINGFACE_SPACE)
|
||||
router = createRouter({ routes: routeRecords, history: createWebHashHistory() })
|
||||
else
|
||||
router = createRouter({ routes: routeRecords, history: createWebHistory() })
|
||||
|
||||
router.beforeEach((to, from) => {
|
||||
if (to.path !== from.path)
|
||||
NProgress.start()
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
NProgress.done()
|
||||
})
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
console.warn('Unhandled rejection:', event.reason)
|
||||
})
|
||||
|
||||
createApp(App)
|
||||
.use(MotionPlugin)
|
||||
// TODO: Fix autoAnimatePlugin type error
|
||||
.use(autoAnimatePlugin as unknown as Plugin)
|
||||
.use(router)
|
||||
.use(pinia)
|
||||
.use(i18n)
|
||||
.use(Tres)
|
||||
.mount('#app')
|
||||
|
||||
if (import.meta.env.DEV && !import.meta.env.SSR) {
|
||||
function captureEvents(el: HTMLElement) {
|
||||
// Force `pointer-events: auto` as DismissableLayer in Reka UI adds
|
||||
// `pointer-events: none` to document body.
|
||||
el.style.pointerEvents = 'auto'
|
||||
|
||||
// We need to capture events inside elements like devtools to prevent them
|
||||
// from leaking to other layers (like DismissableLayer in Reka UI).
|
||||
//
|
||||
// See: https://github.com/unovue/reka-ui/blob/14866201d179b8bae3c8b4346a1ca8eff1c5eaa4/packages/radix-vue/src/DismissableLayer/DismissableLayer.vue#L186-L188
|
||||
el.addEventListener('focus', e => e.stopPropagation(), { capture: true })
|
||||
el.addEventListener('blur', e => e.stopPropagation(), { capture: true })
|
||||
el.addEventListener('pointerdown', e => e.stopPropagation(), { capture: true })
|
||||
}
|
||||
|
||||
const observer = new MutationObserver((mutationsList, observer) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'childList') {
|
||||
const devtoolsContainer = document.getElementById('__vue-devtools-container__')
|
||||
|
||||
if (devtoolsContainer) {
|
||||
captureEvents(devtoolsContainer)
|
||||
observer.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
observer.observe(document.body, { childList: true, subtree: true })
|
||||
|
||||
// Disconnect on timeout in case the MutationObserver is left here forever.
|
||||
// `observer.disconnect()` is idempotent, so it's safe to call it multiple times.
|
||||
setTimeout(() => observer.disconnect(), 15 * 1000)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import messages from '@proj-airi/i18n/locales'
|
||||
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
const languageRemap: Record<string, string> = {
|
||||
'zh-CN': 'zh-Hans',
|
||||
'zh-TW': 'zh-Hant',
|
||||
'zh-HK': 'zh-Hans', // TODO: remove this when zh-Hant is supported
|
||||
'zh-Hant': 'zh-Hans', // TODO: remove this when zh-Hant is supported
|
||||
'en-US': 'en',
|
||||
'en-GB': 'en',
|
||||
'en-AU': 'en',
|
||||
'en': 'en',
|
||||
'es-ES': 'es',
|
||||
'es-MX': 'es',
|
||||
'es-AR': 'es',
|
||||
'es': 'es',
|
||||
'vi': 'vi',
|
||||
'vi-VN': 'vi',
|
||||
'ru': 'ru',
|
||||
'ru-RU': 'ru',
|
||||
'fr': 'fr',
|
||||
'fr-FR': 'fr',
|
||||
}
|
||||
|
||||
function getLocale() {
|
||||
let language = localStorage.getItem('settings/language')
|
||||
|
||||
if (!language) {
|
||||
// Fallback to browser language
|
||||
language = navigator.language || 'en'
|
||||
}
|
||||
|
||||
const languages = Object.keys(messages!)
|
||||
if (languageRemap[language || 'en'] != null) {
|
||||
language = languageRemap[language || 'en']
|
||||
}
|
||||
if (language && languages.includes(language))
|
||||
return language
|
||||
|
||||
return 'en'
|
||||
}
|
||||
|
||||
export const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: getLocale(),
|
||||
fallbackLocale: 'en',
|
||||
messages,
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
import posthog from 'posthog-js'
|
||||
|
||||
posthog.init('phc_pzjziJjrVZpa9SqnQqq0QEKvkmuCPH7GDTA6TbRTEf9', {
|
||||
api_host: 'https://us.i.posthog.com',
|
||||
person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
export { registerSW } from 'virtual:pwa-register'
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { Button } from '@proj-airi/ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div h-full w-full flex flex-col items-center justify-center gap-4>
|
||||
<div text-3xl>
|
||||
Where are we?
|
||||
</div>
|
||||
<div>
|
||||
<Button @click="router.go(-1)">
|
||||
Go Back
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { useDevicesList, useObjectUrl } from '@vueuse/core'
|
||||
import { BufferTarget, MediaStreamAudioTrackSource, Output, QUALITY_MEDIUM, WavOutputFormat } from 'mediabunny'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const { audioInputs } = useDevicesList({ constraints: { audio: true }, requestPermissions: true })
|
||||
const constraintId = ref('')
|
||||
|
||||
async function getMediaStreamTrack(constraint: ConstrainDOMString) {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId: constraint } })
|
||||
return stream.getAudioTracks()[0]
|
||||
}
|
||||
|
||||
let output: Output | undefined
|
||||
let audioInputTrack: MediaStreamAudioTrack | undefined
|
||||
let format: string | undefined
|
||||
|
||||
const recorded = ref<ArrayBuffer[]>([])
|
||||
const recordedUrls = computed(() => recorded.value.map(rec => useObjectUrl(new Blob([rec], { type: format })).value))
|
||||
|
||||
async function handleStart() {
|
||||
audioInputTrack = await getMediaStreamTrack(constraintId.value)
|
||||
output = new Output({ format: new WavOutputFormat(), target: new BufferTarget() })
|
||||
|
||||
const audioSource = new MediaStreamAudioTrackSource(audioInputTrack, { codec: 'pcm-f32', bitrate: QUALITY_MEDIUM })
|
||||
audioSource.errorPromise.catch(console.error)
|
||||
output.addAudioTrack(audioSource)
|
||||
|
||||
format = await output.getMimeType()
|
||||
await output.start()
|
||||
}
|
||||
|
||||
async function handleStop() {
|
||||
await output?.finalize()
|
||||
const bufferTarget = output?.target as BufferTarget | undefined
|
||||
|
||||
if (bufferTarget?.buffer)
|
||||
recorded.value.push(bufferTarget.buffer)
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
output?.cancel()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<select v-model="constraintId">
|
||||
<option value="">
|
||||
Select
|
||||
</option>
|
||||
<option v-for="(item, index) of audioInputs" :key="index" :value="item.deviceId">
|
||||
{{ item.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div space-x-2>
|
||||
<button @click="handleStart">
|
||||
Start
|
||||
</button>
|
||||
<button @click="handleCancel">
|
||||
Cancel
|
||||
</button>
|
||||
<button @click="handleStop">
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<audio v-for="(url, index) in recordedUrls" :key="index" controls>
|
||||
<source :src="url" type="audio/wav">
|
||||
</audio>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,221 @@
|
||||
<script setup lang="ts">
|
||||
import { colorFromElement } from '@proj-airi/stage-ui/libs'
|
||||
import { BasicInputFile } from '@proj-airi/ui'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
|
||||
|
||||
import defaultBackgroundImage from '../../assets/backgrounds/fairy-forest.e17cbc2774.ko-fi.com.avif'
|
||||
|
||||
import { useThemeColor } from '../../composables/theme-color'
|
||||
// Reactive state
|
||||
const isCapturing = ref(false)
|
||||
const extractedColors = ref<string[]>([])
|
||||
const dominantColor = ref('')
|
||||
const topEdgeColors = ref('')
|
||||
|
||||
const imageCleanups = ref<Array<() => void>>([])
|
||||
const imageFiles = ref<File[]>([])
|
||||
const images = computed(() => {
|
||||
if (imageFiles.value.length === 0)
|
||||
return [defaultBackgroundImage]
|
||||
|
||||
return imageFiles.value.map((file) => {
|
||||
const url = URL.createObjectURL(file)
|
||||
imageCleanups.value.push(() => URL.revokeObjectURL(url))
|
||||
|
||||
return url
|
||||
})
|
||||
})
|
||||
|
||||
const mode = ref<'vibrant' | 'html2canvas'>('vibrant')
|
||||
|
||||
// Template refs
|
||||
const imageRef = useTemplateRef<HTMLDivElement>('imageRef')
|
||||
const canvasRef = useTemplateRef<HTMLCanvasElement>('canvas')
|
||||
|
||||
// Computed gradient for top bar blending
|
||||
const topBar = computed(() => {
|
||||
if (mode.value === 'vibrant') {
|
||||
return dominantColor.value
|
||||
}
|
||||
else if (mode.value === 'html2canvas') {
|
||||
return topEdgeColors.value
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
|
||||
// Theme color integration
|
||||
const { updateThemeColor } = useThemeColor(() => topBar.value)
|
||||
|
||||
async function refreshColors() {
|
||||
if (!imageRef.value || images.value.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isCapturing.value = true
|
||||
|
||||
if (imageRef.value instanceof HTMLImageElement && !imageRef.value.complete) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
imageRef.value?.addEventListener('load', () => resolve(), { once: true })
|
||||
imageRef.value?.addEventListener('error', () => reject(new Error('Image failed to load')), { once: true })
|
||||
})
|
||||
}
|
||||
|
||||
const result = await colorFromElement(imageRef.value, {
|
||||
mode: 'both',
|
||||
vibrant: {
|
||||
imageSource: images.value[0],
|
||||
sampleTopRatio: 0.2,
|
||||
},
|
||||
html2canvas: {
|
||||
region: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: imageRef.value.offsetWidth,
|
||||
height: 100,
|
||||
},
|
||||
sampleHeight: 20,
|
||||
sampleStride: 10,
|
||||
scale: 0.5,
|
||||
backgroundColor: null,
|
||||
allowTaint: true,
|
||||
useCORS: true,
|
||||
},
|
||||
})
|
||||
|
||||
extractedColors.value = result.vibrant?.palette ?? []
|
||||
dominantColor.value = result.vibrant?.dominant ?? ''
|
||||
topEdgeColors.value = result.html2canvas?.average ?? ''
|
||||
|
||||
if (canvasRef.value && result.html2canvas?.canvas) {
|
||||
const ctx = canvasRef.value.getContext('2d')
|
||||
if (ctx) {
|
||||
canvasRef.value.width = result.html2canvas.canvas.width
|
||||
canvasRef.value.height = result.html2canvas.canvas.height
|
||||
ctx.drawImage(result.html2canvas.canvas, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
await updateThemeColor()
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Color extraction failed:', error)
|
||||
}
|
||||
finally {
|
||||
isCapturing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-extract colors on mount
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
await refreshColors()
|
||||
})
|
||||
|
||||
watch(images, async () => {
|
||||
await nextTick()
|
||||
await refreshColors()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
imageCleanups.value.forEach(cleanup => cleanup())
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full flex flex-col gap-4">
|
||||
<div class="relative w-full overflow-hidden rounded-xl">
|
||||
<div class="pointer-events-none left-0 right-0 top-0 z-10 flex items-center justify-center backdrop-blur-md" :style="{ background: topBar }">
|
||||
<div class="py-4 text-center text-sm text-white font-medium">
|
||||
Top Area
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
ref="imageRef"
|
||||
:src="images[0]"
|
||||
class="h-full max-h-[calc(100dvh-28rem)] w-full object-cover"
|
||||
>
|
||||
|
||||
<!-- Live2D area placeholder -->
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<BasicInputFile v-model="imageFiles">
|
||||
<div class="rounded-xl bg-black bg-opacity-30 px-5 py-4 text-white backdrop-blur-sm">
|
||||
Replace Image
|
||||
</div>
|
||||
</BasicInputFile>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Debug information -->
|
||||
<div class="debug-info flex flex-col" bg="neutral-100 dark:neutral-900" rounded-xl>
|
||||
<div flex>
|
||||
<!-- Extracted colors -->
|
||||
<div class="rounded-lg p-4" flex-1>
|
||||
<h3 class="mb-1 text-lg" flex items-center gap-2>
|
||||
<span>Node Vibrant</span>
|
||||
<button
|
||||
border-2 border-neutral-300 rounded-xl border-solid px-3 py-1 text-sm font-normal dark:border-neutral-700
|
||||
:class="[
|
||||
mode === 'vibrant' ? 'bg-neutral-100 dark:bg-neutral-700' : '',
|
||||
]"
|
||||
@click="mode = 'vibrant'"
|
||||
>
|
||||
{{ mode === 'vibrant' ? 'Activated' : 'Active' }}
|
||||
</button>
|
||||
</h3>
|
||||
<div class="color-palette flex flex-wrap gap-2">
|
||||
<div
|
||||
v-for="color in extractedColors"
|
||||
:key="color"
|
||||
class="h-12 w-12 cursor-pointer border-2 border-gray-300 rounded rounded-xl transition-transform duration-200 ease-in-out hover:scale-110 dark:border-gray-900"
|
||||
:style="{ backgroundColor: color }"
|
||||
:title="color"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-2 text-sm">
|
||||
Color: <span :style="{ color: dominantColor }">{{ dominantColor }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg p-4" flex-1>
|
||||
<h3 class="mb-1 text-lg" flex items-center gap-2>
|
||||
<span>html2canvas Top sampling</span>
|
||||
<button
|
||||
border-2 border-neutral-300 rounded-xl border-solid px-3 py-1 text-sm font-normal dark:border-neutral-700
|
||||
:class="[
|
||||
mode === 'html2canvas' ? 'bg-neutral-100 dark:bg-neutral-700' : '',
|
||||
]"
|
||||
@click="mode = 'html2canvas'"
|
||||
>
|
||||
{{ mode === 'html2canvas' ? 'Activated' : 'Active' }}
|
||||
</button>
|
||||
</h3>
|
||||
<div class="color-palette flex flex-wrap gap-2">
|
||||
<div
|
||||
class="color-swatch h-12 w-12 border-2 border-gray-300 rounded rounded-xl dark:border-gray-900"
|
||||
:style="{ backgroundColor: topEdgeColors }"
|
||||
:title="topEdgeColors"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-2 text-sm">
|
||||
Color: <span :style="{ color: topEdgeColors }">{{ topEdgeColors }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg p-4" w-full>
|
||||
<h3 class="mb-1 text-lg">
|
||||
Captured Canvas (Debug)
|
||||
</h3>
|
||||
<canvas
|
||||
ref="canvas"
|
||||
class="max-w-full"
|
||||
style="max-height: 100px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,393 @@
|
||||
<script setup lang="ts">
|
||||
import type { PreTrainedModel, Processor } from '@huggingface/transformers'
|
||||
|
||||
import { AutoModel, AutoProcessor, env, RawImage } from '@huggingface/transformers'
|
||||
import { Button, Checkbox, InputFile } from '@proj-airi/ui'
|
||||
import { check } from 'gpuu/webgpu'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const model = ref<PreTrainedModel>()
|
||||
const processor = ref<Processor>()
|
||||
const error = ref<unknown>()
|
||||
const loading = ref(true)
|
||||
const processing = ref(false)
|
||||
const progressPercent = ref(0)
|
||||
const currentProcessingIndex = ref(-1)
|
||||
const autoProcess = ref(false)
|
||||
const previewImage = ref<string | null>(null)
|
||||
const previewPosition = ref({ x: 0, y: 0 })
|
||||
|
||||
interface ImageItem {
|
||||
file: File
|
||||
originalUrl: string
|
||||
processedUrl: string | null
|
||||
status: 'pending' | 'processing' | 'done' | 'error'
|
||||
}
|
||||
|
||||
const imageItems = ref<ImageItem[]>([])
|
||||
const imageFiles = ref<File[]>([])
|
||||
|
||||
const pendingCount = computed(() => imageItems.value.filter(item => item.status === 'pending').length)
|
||||
const doneCount = computed(() => imageItems.value.filter(item => item.status === 'done').length)
|
||||
|
||||
// Watch for new files and add to imageItems
|
||||
watch(imageFiles, (newFiles) => {
|
||||
if (newFiles.length === 0)
|
||||
return
|
||||
|
||||
const existingNames = new Set(imageItems.value.map(item => item.file.name))
|
||||
const newItems: ImageItem[] = newFiles
|
||||
.filter(file => !existingNames.has(file.name))
|
||||
.map(file => ({
|
||||
file,
|
||||
originalUrl: URL.createObjectURL(file),
|
||||
processedUrl: null,
|
||||
status: 'pending' as const,
|
||||
}))
|
||||
|
||||
imageItems.value.push(...newItems)
|
||||
|
||||
// Auto process if enabled
|
||||
if (autoProcess.value && newItems.length > 0 && !processing.value) {
|
||||
processAllImages()
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for autoProcess toggle - process pending images when enabled
|
||||
watch(autoProcess, (enabled) => {
|
||||
if (enabled && !processing.value && pendingCount.value > 0) {
|
||||
processAllImages()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
if (!((await check()).supported)) {
|
||||
throw new Error('WebGPU is not supported in this browser.')
|
||||
}
|
||||
|
||||
const model_id = 'Xenova/modnet'
|
||||
env.backends.onnx.wasm!.proxy = false
|
||||
model.value ??= await AutoModel.from_pretrained(model_id, {
|
||||
device: 'webgpu',
|
||||
})
|
||||
|
||||
processor.value ??= await AutoProcessor.from_pretrained(model_id, {})
|
||||
}
|
||||
catch (err) {
|
||||
error.value = err
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
async function processImage(item: ImageItem, index: number): Promise<void> {
|
||||
if (!model.value || !processor.value)
|
||||
return
|
||||
|
||||
try {
|
||||
item.status = 'processing'
|
||||
currentProcessingIndex.value = index
|
||||
|
||||
// Load image
|
||||
const img = await RawImage.fromURL(item.originalUrl)
|
||||
|
||||
// Pre-process image
|
||||
const { pixel_values } = await processor.value(img)
|
||||
|
||||
// Predict alpha matte
|
||||
const { output } = await model.value({ input: pixel_values })
|
||||
|
||||
const maskData = (await RawImage.fromTensor(output[0].mul(255).to('uint8')).resize(img.width, img.height)).data
|
||||
|
||||
// Create new canvas
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx)
|
||||
return
|
||||
|
||||
// Draw original image output to canvas
|
||||
ctx.drawImage(img.toCanvas(), 0, 0)
|
||||
|
||||
// Update alpha channel
|
||||
const pixelData = ctx.getImageData(0, 0, img.width, img.height)
|
||||
for (let j = 0; j < maskData.length; ++j) {
|
||||
pixelData.data[4 * j + 3] = maskData[j]
|
||||
}
|
||||
|
||||
ctx.putImageData(pixelData, 0, 0)
|
||||
item.processedUrl = canvas.toDataURL('image/png')
|
||||
item.status = 'done'
|
||||
}
|
||||
catch {
|
||||
item.status = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
async function processAllImages() {
|
||||
if (!model.value || !processor.value || processing.value)
|
||||
return
|
||||
|
||||
processing.value = true
|
||||
progressPercent.value = 0
|
||||
|
||||
const pendingItems = imageItems.value.filter(item => item.status === 'pending')
|
||||
const totalImages = pendingItems.length
|
||||
|
||||
for (let i = 0; i < totalImages; ++i) {
|
||||
await processImage(pendingItems[i], imageItems.value.indexOf(pendingItems[i]))
|
||||
progressPercent.value = Math.round(((i + 1) / totalImages) * 100)
|
||||
}
|
||||
|
||||
processing.value = false
|
||||
currentProcessingIndex.value = -1
|
||||
}
|
||||
|
||||
function downloadImage(index: number) {
|
||||
const item = imageItems.value[index]
|
||||
if (!item || !item.processedUrl)
|
||||
return
|
||||
|
||||
// Get original filename and create new filename with suffix
|
||||
const originalFileName = item.file.name
|
||||
const fileNameWithoutExt = originalFileName.substring(0, originalFileName.lastIndexOf('.')) || originalFileName
|
||||
const newFileName = `${fileNameWithoutExt}-background-removed.png`
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = item.processedUrl
|
||||
link.download = newFileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
function downloadAllImages() {
|
||||
const doneItems = imageItems.value.filter(item => item.status === 'done')
|
||||
if (doneItems.length === 0)
|
||||
return
|
||||
|
||||
doneItems.forEach((_, i) => {
|
||||
const index = imageItems.value.indexOf(doneItems[i])
|
||||
setTimeout(() => downloadImage(index), i * 100)
|
||||
})
|
||||
}
|
||||
|
||||
function removeImage(index: number) {
|
||||
const item = imageItems.value[index]
|
||||
if (item.originalUrl)
|
||||
URL.revokeObjectURL(item.originalUrl)
|
||||
imageItems.value.splice(index, 1)
|
||||
}
|
||||
|
||||
function clearAllImages() {
|
||||
imageItems.value.forEach((item) => {
|
||||
if (item.originalUrl)
|
||||
URL.revokeObjectURL(item.originalUrl)
|
||||
})
|
||||
imageItems.value = []
|
||||
}
|
||||
|
||||
function showPreview(url: string, event: MouseEvent) {
|
||||
previewImage.value = url
|
||||
updatePreviewPosition(event)
|
||||
}
|
||||
|
||||
function updatePreviewPosition(event: MouseEvent) {
|
||||
previewPosition.value = {
|
||||
x: event.clientX + 16,
|
||||
y: event.clientY + 16,
|
||||
}
|
||||
}
|
||||
|
||||
function hidePreview() {
|
||||
previewImage.value = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex flex-col gap-4>
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" flex items-center justify-center gap-2 py-8 text-neutral-500>
|
||||
<div i-svg-spinners:ring-resize text-2xl />
|
||||
<span>Loading model...</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<div v-else-if="error" rounded-lg bg-red-100 p-4 text-red-700 dark:bg-red-900 dark:text-red-200>
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<template v-else>
|
||||
<!-- File upload area -->
|
||||
<InputFile v-model="imageFiles" accept="image/*" multiple w-full />
|
||||
|
||||
<!-- Controls -->
|
||||
<div flex flex-wrap items-center justify-between gap-4>
|
||||
<div flex items-center gap-3>
|
||||
<label flex cursor-pointer items-center gap-2>
|
||||
<Checkbox v-model="autoProcess" />
|
||||
<span text-sm>Auto process on upload</span>
|
||||
</label>
|
||||
</div>
|
||||
<div flex gap-2>
|
||||
<Button
|
||||
v-if="pendingCount > 0"
|
||||
:label="processing ? `Processing... ${progressPercent}%` : `Process ${pendingCount} image${pendingCount > 1 ? 's' : ''}`"
|
||||
:disabled="processing || !model"
|
||||
:loading="processing"
|
||||
@click="processAllImages"
|
||||
/>
|
||||
<Button
|
||||
v-if="doneCount > 0"
|
||||
variant="secondary"
|
||||
:label="`Download All (${doneCount})`"
|
||||
icon="i-solar:download-minimalistic-bold"
|
||||
@click="downloadAllImages"
|
||||
/>
|
||||
<Button
|
||||
v-if="imageItems.length > 0"
|
||||
variant="secondary-muted"
|
||||
label="Clear All"
|
||||
icon="i-solar:trash-bin-trash-line-duotone"
|
||||
@click="clearAllImages"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Table -->
|
||||
<div overflow-x-auto rounded-lg border="1 solid neutral-200 dark:neutral-700">
|
||||
<table w-full text-left text-sm>
|
||||
<thead bg="neutral-100 dark:neutral-800">
|
||||
<tr>
|
||||
<th px-4 py-3 font-medium>
|
||||
Original
|
||||
</th>
|
||||
<th px-4 py-3 font-medium>
|
||||
Processed
|
||||
</th>
|
||||
<th px-4 py-3 font-medium>
|
||||
Filename
|
||||
</th>
|
||||
<th px-4 py-3 font-medium>
|
||||
Status
|
||||
</th>
|
||||
<th px-4 py-3 font-medium>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="imageItems.length === 0">
|
||||
<td colspan="5" px-4 py-8 text-center text-neutral-400>
|
||||
No images uploaded yet
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="(item, index) in imageItems"
|
||||
:key="item.file.name"
|
||||
border="t 1 solid neutral-200 dark:neutral-700"
|
||||
:class="item.status === 'processing' ? 'bg-emerald-50 dark:bg-emerald-900/20' : ''"
|
||||
>
|
||||
<!-- Original thumbnail -->
|
||||
<td px-4 py-3>
|
||||
<div
|
||||
h-16 w-16 cursor-pointer overflow-hidden rounded-lg border="1 solid neutral-200 dark:neutral-700"
|
||||
@mouseenter="showPreview(item.originalUrl, $event)"
|
||||
@mousemove="updatePreviewPosition"
|
||||
@mouseleave="hidePreview"
|
||||
>
|
||||
<img :src="item.originalUrl" h-full w-full object-cover>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Processed thumbnail -->
|
||||
<td px-4 py-3>
|
||||
<div
|
||||
h-16 w-16 overflow-hidden rounded-lg border="1 solid neutral-200 dark:neutral-700"
|
||||
:class="item.processedUrl ? 'cursor-pointer' : ''"
|
||||
bg="[url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%3E%3Crect%20width%3D%228%22%20height%3D%228%22%20fill%3D%22%23ccc%22%2F%3E%3Crect%20x%3D%228%22%20y%3D%228%22%20width%3D%228%22%20height%3D%228%22%20fill%3D%22%23ccc%22%2F%3E%3C%2Fsvg%3E')]"
|
||||
@mouseenter="item.processedUrl && showPreview(item.processedUrl, $event)"
|
||||
@mousemove="updatePreviewPosition"
|
||||
@mouseleave="hidePreview"
|
||||
>
|
||||
<img v-if="item.processedUrl" :src="item.processedUrl" h-full w-full object-cover>
|
||||
<div v-else-if="item.status === 'processing'" h-full w-full flex items-center justify-center bg="neutral-100 dark:neutral-800">
|
||||
<div i-svg-spinners:ring-resize text-emerald-500 />
|
||||
</div>
|
||||
<div v-else h-full w-full flex items-center justify-center bg="neutral-100 dark:neutral-800" text-neutral-400>
|
||||
-
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Filename -->
|
||||
<td max-w-48 truncate px-4 py-3 :title="item.file.name">
|
||||
{{ item.file.name }}
|
||||
</td>
|
||||
|
||||
<!-- Status -->
|
||||
<td px-4 py-3>
|
||||
<span
|
||||
inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs font-medium
|
||||
:class="{
|
||||
'bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-400': item.status === 'pending',
|
||||
'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-400': item.status === 'processing',
|
||||
'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-400': item.status === 'done',
|
||||
'bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-400': item.status === 'error',
|
||||
}"
|
||||
>
|
||||
<div v-if="item.status === 'processing'" i-svg-spinners:ring-resize text-xs />
|
||||
<div v-else-if="item.status === 'done'" i-solar:check-circle-bold text-xs />
|
||||
<div v-else-if="item.status === 'error'" i-solar:close-circle-bold text-xs />
|
||||
<div v-else i-solar:clock-circle-linear text-xs />
|
||||
{{ item.status === 'pending' ? 'Pending' : item.status === 'processing' ? 'Processing' : item.status === 'done' ? 'Done' : 'Error' }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Actions -->
|
||||
<td px-4 py-3>
|
||||
<div flex gap-2>
|
||||
<Button
|
||||
v-if="item.status === 'done'"
|
||||
size="sm"
|
||||
icon="i-solar:download-minimalistic-bold"
|
||||
@click="downloadImage(index)"
|
||||
/>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
icon="i-solar:trash-bin-trash-bold"
|
||||
:disabled="item.status === 'processing'"
|
||||
@click="removeImage(index)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Preview tooltip -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="previewImage"
|
||||
|
||||
pointer-events-none fixed z-50 overflow-hidden rounded-xl shadow-2xl
|
||||
border="1 solid neutral-200 dark:neutral-700"
|
||||
bg="[url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%3E%3Crect%20width%3D%228%22%20height%3D%228%22%20fill%3D%22%23ccc%22%2F%3E%3Crect%20x%3D%228%22%20y%3D%228%22%20width%3D%228%22%20height%3D%228%22%20fill%3D%22%23ccc%22%2F%3E%3C%2Fsvg%3E')]"
|
||||
:style="{
|
||||
left: `${previewPosition.x}px`,
|
||||
top: `${previewPosition.y}px`,
|
||||
maxWidth: '400px',
|
||||
maxHeight: '400px',
|
||||
}"
|
||||
>
|
||||
<img :src="previewImage" max-h-96 max-w-96 object-contain>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,179 @@
|
||||
<script setup lang="ts">
|
||||
import { refDebounced, refThrottled, useElementBounding, usePointer, useResizeObserver } from '@vueuse/core'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const minimumDataPoints = 6
|
||||
|
||||
interface Point {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
const canvasContainerRef = ref<HTMLDivElement>()
|
||||
const canvasRef = ref<HTMLCanvasElement>()
|
||||
const canvasContext = ref<CanvasRenderingContext2D | undefined | null>()
|
||||
|
||||
const { x, y } = usePointer({ target: canvasRef })
|
||||
const bounding = useElementBounding(canvasContainerRef, { immediate: true, windowResize: true })
|
||||
|
||||
const pointComputed = computed(() => {
|
||||
return {
|
||||
x: x.value,
|
||||
y: y.value,
|
||||
}
|
||||
})
|
||||
|
||||
useResizeObserver(canvasContainerRef, (entries) => {
|
||||
const entry = entries[0]
|
||||
const { width, height } = entry.contentRect
|
||||
|
||||
if (!canvasRef.value)
|
||||
return
|
||||
|
||||
canvasRef.value.width = width
|
||||
canvasRef.value.height = height
|
||||
})
|
||||
|
||||
watch(canvasContainerRef, (el) => {
|
||||
if (!el)
|
||||
return
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = bounding.width.value
|
||||
canvas.height = bounding.height.value
|
||||
canvas.style.objectFit = 'contain'
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
canvasContext.value = ctx
|
||||
|
||||
canvasRef.value = canvas
|
||||
canvasContainerRef.value?.appendChild(canvas)
|
||||
})
|
||||
|
||||
const pointThrottled = refThrottled(pointComputed, 50)
|
||||
const pointDebounced = refDebounced(pointComputed, 50)
|
||||
|
||||
const lastPoint = ref<Point>(pointDebounced.value)
|
||||
const distance = ref(0)
|
||||
const count = ref(0)
|
||||
const show = ref(false)
|
||||
|
||||
const lastPoints = ref<Point[]>([])
|
||||
const isCircle = ref(false)
|
||||
|
||||
watch([x, y], () => {
|
||||
if (!canvasContext.value)
|
||||
return
|
||||
|
||||
canvasContext.value.beginPath()
|
||||
canvasContext.value.fillStyle = 'red'
|
||||
// 20: padding left
|
||||
// 40: padding top
|
||||
// 40: header
|
||||
canvasContext.value.arc(x.value - 20, y.value - 40 - 40, 4, 0, 2 * Math.PI)
|
||||
canvasContext.value.closePath()
|
||||
canvasContext.value.fill()
|
||||
})
|
||||
|
||||
watch(pointDebounced, () => {
|
||||
isCircle.value = false
|
||||
if (canvasContext.value) {
|
||||
canvasContext.value.reset()
|
||||
}
|
||||
|
||||
// Use standard deviation of radii to check for circularity to determine if points form a circle
|
||||
if (lastPoints.value.length >= minimumDataPoints) {
|
||||
// Calculate mean center point
|
||||
const meanX = lastPoints.value.reduce((sum, p) => sum + p.x, 0) / lastPoints.value.length
|
||||
const meanY = lastPoints.value.reduce((sum, p) => sum + p.y, 0) / lastPoints.value.length
|
||||
|
||||
// Calculate average radius and deviation
|
||||
const radii = lastPoints.value.map(p =>
|
||||
Math.sqrt((p.x - meanX) ** 2 + (p.y - meanY) ** 2),
|
||||
)
|
||||
const avgRadius = radii.reduce((sum, r) => sum + r, 0) / radii.length
|
||||
|
||||
// Calculate standard deviation of radii
|
||||
const variance = radii.reduce((sum, r) => sum + (r - avgRadius) ** 2, 0) / radii.length
|
||||
const stdDev = Math.sqrt(variance)
|
||||
|
||||
// If points roughly form a circle (low standard deviation)
|
||||
if (stdDev < 20) {
|
||||
count.value++
|
||||
isCircle.value = true
|
||||
}
|
||||
}
|
||||
|
||||
lastPoints.value = []
|
||||
|
||||
// if (lastPoint.value) {
|
||||
// distance.value = Math.sqrt((point.x - lastPoint.value.x) ** 2 + (point.y - lastPoint.value.y) ** 2)
|
||||
|
||||
// if (distance.value < 20) {
|
||||
// count.value++
|
||||
|
||||
// if (count.value >= 1) {
|
||||
// count.value = 0
|
||||
// show.value = true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// lastPoint.value = point
|
||||
})
|
||||
|
||||
watch(pointThrottled, (point) => {
|
||||
lastPoints.value.push(point)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div h="[calc(100dvh-40px)]">
|
||||
<div relative h-full>
|
||||
<div bg="neutral-100/50 dark:neutral-900/50" absolute inset-0 h-fit rounded-xl px-3 py-2 font-mono shadow-md backdrop-blur-md grid="~ cols-[150px_1fr]">
|
||||
<div text="neutral-400 dark:neutral-600">
|
||||
pointThrottled:
|
||||
</div>
|
||||
<div>{{ pointThrottled }}</div>
|
||||
<div text="neutral-400 dark:neutral-600">
|
||||
pointDebounced:
|
||||
</div>
|
||||
<div>{{ pointDebounced }}</div>
|
||||
|
||||
<div text="neutral-400 dark:neutral-600">
|
||||
lastPoint:
|
||||
</div>
|
||||
<div>{{ lastPoint }}</div>
|
||||
<div text="neutral-400 dark:neutral-600">
|
||||
distance:
|
||||
</div>
|
||||
<div>{{ distance }}</div>
|
||||
<div text="neutral-400 dark:neutral-600">
|
||||
count:
|
||||
</div>
|
||||
<div>{{ count }}</div>
|
||||
|
||||
<div text="neutral-400 dark:neutral-600">
|
||||
show:
|
||||
</div>
|
||||
<div>{{ show }}</div>
|
||||
|
||||
<div text="neutral-400 dark:neutral-600">
|
||||
lastPoints:
|
||||
</div>
|
||||
<div>{{ lastPoints }}</div>
|
||||
<div text="neutral-400 dark:neutral-600">
|
||||
isCircle:
|
||||
</div>
|
||||
<div>{{ isCircle }}</div>
|
||||
</div>
|
||||
|
||||
<div ref="canvasContainerRef" h-full w-full />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: plain
|
||||
</route>
|
||||
@@ -0,0 +1,311 @@
|
||||
<script setup lang="ts">
|
||||
import type { TTSChunkItem } from '@proj-airi/stage-ui/utils/tts'
|
||||
import type { ChatProvider, SpeechProviderWithExtraOptions } from '@xsai-ext/providers/utils'
|
||||
|
||||
import { ThreeScene } from '@proj-airi/stage-ui-three'
|
||||
import { animations } from '@proj-airi/stage-ui-three/assets/vrm'
|
||||
import { useDelayMessageQueue, useEmotionsMessageQueue, usePipelineCharacterSpeechPlaybackQueueStore, usePipelineWorkflowTextSegmentationStore } from '@proj-airi/stage-ui/composables/queues'
|
||||
import { llmInferenceEndToken } from '@proj-airi/stage-ui/constants'
|
||||
import { EMOTION_EmotionMotionName_value, EMOTION_VRMExpressionName_value, EmotionThinkMotionName } from '@proj-airi/stage-ui/constants/emotions'
|
||||
import { useAudioContext, useSpeakingStore } from '@proj-airi/stage-ui/stores/audio'
|
||||
import { useChatStore } from '@proj-airi/stage-ui/stores/chat'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { createQueue } from '@proj-airi/stage-ui/utils/queue'
|
||||
import { generateSpeech } from '@xsai/generate-speech'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
// VRM scene refs
|
||||
const sceneRef = ref<InstanceType<typeof ThreeScene>>()
|
||||
|
||||
// Playback + lip sync (VRM uses currentAudioSource)
|
||||
const characterSpeechPlaybackQueue = usePipelineCharacterSpeechPlaybackQueueStore()
|
||||
const { connectAudioContext, connectAudioAnalyser, clearAll, onPlaybackStarted, onPlaybackFinished } = characterSpeechPlaybackQueue
|
||||
const { currentAudioSource, playbackQueue } = storeToRefs(characterSpeechPlaybackQueue)
|
||||
|
||||
// Audio context / analyser
|
||||
const { audioContext } = useAudioContext()
|
||||
connectAudioContext(audioContext)
|
||||
const audioAnalyser = ref<AnalyserNode>()
|
||||
function setupAnalyser() {
|
||||
if (!audioAnalyser.value) {
|
||||
audioAnalyser.value = audioContext.createAnalyser()
|
||||
connectAudioAnalyser(audioAnalyser.value)
|
||||
}
|
||||
}
|
||||
|
||||
// Settings + force VRM model
|
||||
const settingsStore = useSettings()
|
||||
const { stageModelRenderer, stageModelSelected, stageModelSelectedUrl, stageViewControlsEnabled } = storeToRefs(settingsStore)
|
||||
onMounted(async () => {
|
||||
// Preserve existing VRM selection if available; otherwise fall back to preset VRM
|
||||
const needsFallback = !stageModelSelectedUrl.value || stageModelRenderer.value !== 'vrm'
|
||||
if (needsFallback)
|
||||
stageModelSelected.value = 'preset-vrm-1'
|
||||
|
||||
await settingsStore.updateStageModel()
|
||||
setupAnalyser()
|
||||
})
|
||||
|
||||
// Speech
|
||||
const providersStore = useProvidersStore()
|
||||
const speechStore = useSpeechStore()
|
||||
const { activeSpeechProvider, activeSpeechVoice, activeSpeechModel, ssmlEnabled, pitch } = storeToRefs(speechStore)
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const { activeProvider: activeChatProvider, activeModel: activeChatModel } = storeToRefs(consciousnessStore)
|
||||
|
||||
// Text segmentation
|
||||
const textSegmentationStore = usePipelineWorkflowTextSegmentationStore()
|
||||
const { onTextSegmented, clearHooks: clearTextSegmentationHooks } = textSegmentationStore
|
||||
const { textSegmentationQueue } = storeToRefs(textSegmentationStore)
|
||||
clearTextSegmentationHooks()
|
||||
|
||||
// Emotion/delay queues (special tokens)
|
||||
const delaysQueue = useDelayMessageQueue()
|
||||
const emotionMessageQueue = useEmotionsMessageQueue(createQueue({ handlers: [] }))
|
||||
emotionMessageQueue.on('enqueue', (token) => {
|
||||
log(` - special 入队:${token}`)
|
||||
})
|
||||
emotionMessageQueue.on('dequeue', (token) => {
|
||||
log(`special 出队处理:${token}`)
|
||||
})
|
||||
|
||||
// State
|
||||
const { mouthOpenSize } = storeToRefs(useSpeakingStore())
|
||||
const nowSpeaking = ref(false)
|
||||
const currentMotion = ref<{ group: string }>({ group: EmotionThinkMotionName })
|
||||
const logLines = ref<string[]>([])
|
||||
const chatInput = ref('')
|
||||
const chatStore = useChatStore()
|
||||
const chatMessages = computed(() => {
|
||||
return chatStore.messages
|
||||
.filter(msg => msg.role !== 'system')
|
||||
.map((msg) => {
|
||||
const text = typeof msg.content === 'string'
|
||||
? msg.content
|
||||
: Array.isArray(msg.content)
|
||||
? msg.content.map((part: any) => typeof part === 'string' ? part : part.text ?? '').join('')
|
||||
: JSON.stringify(msg.content ?? '')
|
||||
return { role: msg.role as 'user' | 'assistant', text }
|
||||
})
|
||||
})
|
||||
|
||||
function log(line: string) {
|
||||
logLines.value = [line, ...logLines.value].slice(0, 50)
|
||||
}
|
||||
|
||||
// TTS generation handler
|
||||
async function handleSpeechGeneration(ctx: { data: TTSChunkItem }) {
|
||||
try {
|
||||
if (!activeSpeechProvider.value || !activeSpeechVoice.value) {
|
||||
console.warn('No active speech provider configured')
|
||||
return
|
||||
}
|
||||
const provider = await providersStore.getProviderInstance(activeSpeechProvider.value) as SpeechProviderWithExtraOptions<string, any>
|
||||
if (!provider) {
|
||||
console.error('Failed to initialize speech provider')
|
||||
return
|
||||
}
|
||||
if (ctx.data.chunk === '' && !ctx.data.special)
|
||||
return
|
||||
if (ctx.data.chunk === '' && ctx.data.special) {
|
||||
// log(`特殊标记:${ctx.data.special}`)
|
||||
emotionMessageQueue.enqueue(ctx.data.special)
|
||||
return
|
||||
}
|
||||
const providerConfig = providersStore.getProviderConfig(activeSpeechProvider.value)
|
||||
const input = ssmlEnabled.value
|
||||
? speechStore.generateSSML(ctx.data.chunk, activeSpeechVoice.value, { ...providerConfig, pitch: pitch.value })
|
||||
: ctx.data.chunk
|
||||
|
||||
const res = await generateSpeech({
|
||||
...provider.speech(activeSpeechModel.value, providerConfig),
|
||||
input,
|
||||
voice: activeSpeechVoice.value.id,
|
||||
})
|
||||
|
||||
const audioBuffer = await audioContext.decodeAudioData(res)
|
||||
log(` - 排队:${ctx.data.chunk}${ctx.data.special ? ` [special: ${ctx.data.special}]` : ''}`)
|
||||
playbackQueue.value.enqueue({ audioBuffer, text: ctx.data.chunk, special: ctx.data.special })
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Speech generation failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const ttsQueue = createQueue<TTSChunkItem>({
|
||||
handlers: [
|
||||
handleSpeechGeneration,
|
||||
],
|
||||
})
|
||||
|
||||
// text segmentation hooks
|
||||
onTextSegmented((chunkItem) => {
|
||||
ttsQueue.enqueue(chunkItem)
|
||||
})
|
||||
|
||||
async function sendChat() {
|
||||
const content = chatInput.value.trim()
|
||||
if (!content)
|
||||
return
|
||||
|
||||
const provider = await providersStore.getProviderInstance(activeChatProvider.value)
|
||||
if (!provider || !activeChatModel.value) {
|
||||
log('未配置聊天模型或 provider')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await chatStore.send(content, {
|
||||
model: activeChatModel.value,
|
||||
chatProvider: provider as ChatProvider,
|
||||
})
|
||||
chatInput.value = ''
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
log('发送到 LLM 失败')
|
||||
}
|
||||
}
|
||||
|
||||
function resetChat() {
|
||||
chatStore.cleanupMessages()
|
||||
chatInput.value = ''
|
||||
logLines.value = []
|
||||
clearAll()
|
||||
}
|
||||
|
||||
// Chat hooks (reuse Stage pipeline but Live2D removed)
|
||||
const { onBeforeMessageComposed, onBeforeSend, onTokenLiteral, onTokenSpecial, onStreamEnd } = chatStore
|
||||
const chatHookCleanups: Array<() => void> = []
|
||||
|
||||
chatHookCleanups.push(onBeforeMessageComposed(async () => {
|
||||
clearAll()
|
||||
setupAnalyser()
|
||||
logLines.value = []
|
||||
}))
|
||||
|
||||
chatHookCleanups.push(onBeforeSend(async () => {
|
||||
currentMotion.value = { group: EmotionThinkMotionName }
|
||||
}))
|
||||
|
||||
chatHookCleanups.push(onTokenLiteral(async (literal) => {
|
||||
textSegmentationQueue.value.enqueue({ type: 'literal', value: literal })
|
||||
}))
|
||||
|
||||
chatHookCleanups.push(onTokenSpecial(async (special) => {
|
||||
textSegmentationQueue.value.enqueue({ type: 'special', value: special })
|
||||
}))
|
||||
|
||||
chatHookCleanups.push(onStreamEnd(async () => {
|
||||
delaysQueue.enqueue(llmInferenceEndToken)
|
||||
}))
|
||||
|
||||
// Wire playback to VRM + logs
|
||||
onPlaybackFinished(({ special }) => {
|
||||
nowSpeaking.value = false
|
||||
mouthOpenSize.value = 0
|
||||
if (special) {
|
||||
log(`播放结束,special: ${special}`)
|
||||
const motion = EMOTION_EmotionMotionName_value[special as keyof typeof EMOTION_EmotionMotionName_value]
|
||||
const expression = EMOTION_VRMExpressionName_value[special as keyof typeof EMOTION_VRMExpressionName_value]
|
||||
if (motion)
|
||||
currentMotion.value = { group: motion }
|
||||
if (expression)
|
||||
sceneRef.value?.setExpression(expression)
|
||||
}
|
||||
})
|
||||
|
||||
onPlaybackStarted(({ text }) => {
|
||||
nowSpeaking.value = true
|
||||
log(`播放开始:${text}`)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
chatHookCleanups.forEach(dispose => dispose?.())
|
||||
clearAll()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div p-4 space-y-4>
|
||||
<div text-lg font-600>
|
||||
Performance Layer Playground(复刻 Stage,去掉 Live2D)
|
||||
</div>
|
||||
<div grid gap-4 lg:grid-cols-2>
|
||||
<div border="1 solid neutral-300/40 dark:neutral-700/40" h-100 min-h-80 overflow-hidden rounded-2xl>
|
||||
<ThreeScene
|
||||
v-if="stageModelRenderer === 'vrm'"
|
||||
ref="sceneRef"
|
||||
:model-src="stageModelSelectedUrl"
|
||||
:idle-animation="animations.idleLoop.toString()"
|
||||
:current-audio-source="currentAudioSource"
|
||||
:show-axes="stageViewControlsEnabled"
|
||||
:paused="false"
|
||||
@error="console.error"
|
||||
/>
|
||||
<div v-else p-4 text-sm text-red-500>
|
||||
请选择 VRM 模型(当前模型类型不支持)。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-neutral-300/50 rounded-xl p-3 text-xs leading-relaxed space-y-3 dark:border-neutral-700/60">
|
||||
<div font-600>
|
||||
聊天 / 播放
|
||||
</div>
|
||||
<div class="h-60 overflow-auto border border-neutral-200/60 rounded-lg p-2 dark:border-neutral-700/60">
|
||||
<div v-for="(msg, idx) in chatMessages" :key="idx" class="mb-2">
|
||||
<div class="text-[11px] text-neutral-500">
|
||||
{{ msg.role === 'user' ? 'User' : 'AIRI' }}
|
||||
</div>
|
||||
<div class="whitespace-pre-wrap break-words text-sm">
|
||||
{{ msg.text }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!chatMessages.length" class="text-sm text-neutral-500">
|
||||
输入消息进行对话。
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="chatInput"
|
||||
class="flex-1 border border-neutral-300/60 rounded-lg bg-white px-3 py-2 text-sm dark:bg-neutral-900/60"
|
||||
placeholder="输入消息,点击发送"
|
||||
@keyup.enter="sendChat"
|
||||
>
|
||||
<button
|
||||
class="rounded-lg bg-primary-500 px-3 py-2 text-white disabled:bg-neutral-400"
|
||||
:disabled="!chatInput.trim()"
|
||||
@click="sendChat"
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
<button
|
||||
class="border border-neutral-300/60 rounded-lg px-3 py-2 text-sm"
|
||||
@click="resetChat"
|
||||
>
|
||||
重置对话
|
||||
</button>
|
||||
</div>
|
||||
<div class="border border-neutral-200/60 rounded-lg p-2 dark:border-neutral-700/60">
|
||||
<div mb-1 font-600>
|
||||
播放队列 / 日志
|
||||
</div>
|
||||
<ul class="max-h-60 overflow-auto space-y-1">
|
||||
<li v-for="line in logLines" :key="line">
|
||||
{{ line }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
</route>
|
||||
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import { Screen } from '@proj-airi/stage-ui/components'
|
||||
import { Live2DCanvas, Live2DModel } from '@proj-airi/stage-ui/components/scenes'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const live2dCanvasRef = ref<InstanceType<typeof Live2DCanvas>>()
|
||||
const live2dModelRef = ref<InstanceType<typeof Live2DModel>>()
|
||||
|
||||
const settingsStore = useSettings()
|
||||
const { stageModelSelectedUrl } = storeToRefs(settingsStore)
|
||||
const motion = ref<string>('idle')
|
||||
const motionGroupsList = ref<{
|
||||
motionName: string
|
||||
motionIndex: number
|
||||
fileName: string
|
||||
}[]>([])
|
||||
|
||||
function download(href: string, name: string) {
|
||||
const link = document.createElement('a')
|
||||
link.href = href
|
||||
link.download = name
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
function handleSetMotion(motionName: string) {
|
||||
live2dModelRef.value?.setMotion(motionName)
|
||||
}
|
||||
|
||||
watch(live2dModelRef, (model) => {
|
||||
motionGroupsList.value = model?.listMotionGroups() || []
|
||||
}, { immediate: true })
|
||||
|
||||
function handleModelLoaded() {
|
||||
if (live2dModelRef.value) {
|
||||
live2dModelRef.value?.setMotion(motion.value)
|
||||
motionGroupsList.value = live2dModelRef.value.listMotionGroups()
|
||||
}
|
||||
}
|
||||
|
||||
function handleShot() {
|
||||
if (!live2dCanvasRef.value || !live2dModelRef.value)
|
||||
return
|
||||
|
||||
const canvas = live2dCanvasRef.value.canvasElement()
|
||||
const dataUrl = canvas!.toDataURL('image/png')
|
||||
download(dataUrl, 'live2d-screenshot.png')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex flex-col items-center gap-4>
|
||||
<div h-full w-full>
|
||||
<Screen v-slot="{ width, height }" relative min-h-70dvh>
|
||||
<Live2DCanvas
|
||||
ref="live2dCanvasRef"
|
||||
v-slot="{ app }"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:resolution="3"
|
||||
rounded-full
|
||||
>
|
||||
<Live2DModel
|
||||
ref="live2dModelRef"
|
||||
:model-src="stageModelSelectedUrl"
|
||||
:app="app"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:focus-at="{ x: width / 2, y: height / 2 }"
|
||||
@model-loaded="handleModelLoaded"
|
||||
/>
|
||||
</Live2DCanvas>
|
||||
</Screen>
|
||||
</div>
|
||||
<div>
|
||||
<select v-model="motion" rounded-lg px-3 py-2 @change="handleSetMotion(motion)">
|
||||
<option v-for="motionItem in motionGroupsList" :key="motionItem.motionIndex" :value="motionItem.motionName">
|
||||
{{ motionItem.fileName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div border="2px solid black dark:white" flex items-center justify-center rounded-full p-1>
|
||||
<button
|
||||
class="h-15 w-15 md:h-18 md:w-18"
|
||||
bg="black active:neutral-950 dark:white dark:active:neutral-50"
|
||||
rounded-full outline-none transition-colors duration-200 ease-in-out
|
||||
@click="handleShot"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
import { useMagicKeys } from '@vueuse/core'
|
||||
import { computed, defineComponent, h } from 'vue'
|
||||
|
||||
const {
|
||||
shift,
|
||||
control,
|
||||
escape,
|
||||
tab,
|
||||
v,
|
||||
u,
|
||||
e,
|
||||
s,
|
||||
v_u_e,
|
||||
u_s_e,
|
||||
i_Shift_Alt,
|
||||
a_Shift_Alt,
|
||||
n_Shift_Alt,
|
||||
current,
|
||||
} = useMagicKeys()
|
||||
const keys = computed(() => Array.from(current))
|
||||
|
||||
const Key = defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
render() {
|
||||
return h('div', {
|
||||
class: [
|
||||
'font-mono px-4 py-2 rounded',
|
||||
this.value
|
||||
? 'opacity-100 text-primary bg-primary bg-opacity-15'
|
||||
: 'opacity-50 bg-gray-600 bg-opacity-10 dark:(bg-gray-400 bg-opacity-10)',
|
||||
],
|
||||
}, this.$slots.default?.())
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col md:flex-row">
|
||||
<img
|
||||
class="m-auto h-38 transform py-8 transition duration-500"
|
||||
:class="{ 'opacity-0': !v_u_e, 'rotate-180': shift }"
|
||||
>
|
||||
|
||||
<div>
|
||||
<div class="mb-5 mt-0 text-center">
|
||||
Press the following keys to test out
|
||||
</div>
|
||||
<div class="flex justify-center gap-3">
|
||||
<Key :value="v">
|
||||
V
|
||||
</Key>
|
||||
<Key :value="u">
|
||||
u
|
||||
</Key>
|
||||
<Key :value="e">
|
||||
e
|
||||
</Key>
|
||||
<div class="mx-1" />
|
||||
<Key :value="u">
|
||||
U
|
||||
</Key>
|
||||
<Key :value="s">
|
||||
s
|
||||
</Key>
|
||||
<Key :value="e">
|
||||
e
|
||||
</Key>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex justify-center gap-3">
|
||||
<Key :value="escape">
|
||||
Escape
|
||||
</Key>
|
||||
<Key :value="shift">
|
||||
Shift
|
||||
</Key>
|
||||
<Key :value="control">
|
||||
Control
|
||||
</Key>
|
||||
<Key :value="tab">
|
||||
Tab
|
||||
</Key>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex justify-center gap-3">
|
||||
<Key :value="v_u_e">
|
||||
Vue
|
||||
</Key>
|
||||
<Key :value="u_s_e">
|
||||
Use
|
||||
</Key>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex flex-col items-center gap-3">
|
||||
<Key :value="i_Shift_Alt">
|
||||
Shift + Alt + I
|
||||
</Key>
|
||||
<Key :value="a_Shift_Alt">
|
||||
Shift + Alt + A
|
||||
</Key>
|
||||
<Key :value="n_Shift_Alt">
|
||||
Shift + Alt + N
|
||||
</Key>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<div>Keys Pressed</div>
|
||||
<div class="mt-2 min-h-1.5em flex justify-center space-x-1">
|
||||
<code
|
||||
v-for="key in keys"
|
||||
:key="key"
|
||||
class="font-mono"
|
||||
>
|
||||
{{ key }}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
class="m-auto h-38 transform py-8 transition duration-500"
|
||||
:class="{ 'opacity-0': !u_s_e, 'rotate-180': shift }"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,213 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChatProvider } from '@xsai-ext/providers/utils'
|
||||
|
||||
import workletUrl from '@proj-airi/stage-ui/workers/vad/process.worklet?worker&url'
|
||||
|
||||
import { WidgetStage } from '@proj-airi/stage-ui/components/scenes'
|
||||
import { useAudioRecorder } from '@proj-airi/stage-ui/composables/audio/audio-recorder'
|
||||
import { useVAD } from '@proj-airi/stage-ui/stores/ai/models/vad'
|
||||
import { useChatStore } from '@proj-airi/stage-ui/stores/chat'
|
||||
import { useLive2d } from '@proj-airi/stage-ui/stores/live2d'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
import { useHearingSpeechInputPipeline } from '@proj-airi/stage-ui/stores/modules/hearing'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { breakpointsTailwind, useBreakpoints, useMouse } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
|
||||
|
||||
import Header from '../components/Layouts/Header.vue'
|
||||
import InteractiveArea from '../components/Layouts/InteractiveArea.vue'
|
||||
import MobileHeader from '../components/Layouts/MobileHeader.vue'
|
||||
import MobileInteractiveArea from '../components/Layouts/MobileInteractiveArea.vue'
|
||||
|
||||
import { BackgroundProvider } from '../components/Backgrounds'
|
||||
import { useBackgroundThemeColor } from '../composables/theme-color'
|
||||
import { useBackgroundStore } from '../stores/background'
|
||||
|
||||
const paused = ref(false)
|
||||
|
||||
function handleSettingsOpen(open: boolean) {
|
||||
paused.value = open
|
||||
}
|
||||
|
||||
const positionCursor = useMouse()
|
||||
const { scale, position, positionInPercentageString } = storeToRefs(useLive2d())
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||
const isMobile = breakpoints.smaller('md')
|
||||
|
||||
const backgroundStore = useBackgroundStore()
|
||||
const { selectedOption, sampledColor } = storeToRefs(backgroundStore)
|
||||
const backgroundSurface = useTemplateRef<InstanceType<typeof BackgroundProvider>>('backgroundSurface')
|
||||
|
||||
const { syncBackgroundTheme } = useBackgroundThemeColor({ backgroundSurface, selectedOption, sampledColor })
|
||||
onMounted(() => syncBackgroundTheme())
|
||||
|
||||
// Audio + transcription pipeline (mirrors stage-tamagotchi)
|
||||
const settingsAudioDeviceStore = useSettingsAudioDevice()
|
||||
const { stream, enabled } = storeToRefs(settingsAudioDeviceStore)
|
||||
const { startRecord, stopRecord, onStopRecord } = useAudioRecorder(stream)
|
||||
const hearingPipeline = useHearingSpeechInputPipeline()
|
||||
const { transcribeForRecording, transcribeForMediaStream } = hearingPipeline
|
||||
const { supportsStreamInput } = storeToRefs(hearingPipeline)
|
||||
const providersStore = useProvidersStore()
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const { activeProvider: activeChatProvider, activeModel: activeChatModel } = storeToRefs(consciousnessStore)
|
||||
const chatStore = useChatStore()
|
||||
|
||||
const shouldUseStreamInput = computed(() => supportsStreamInput.value && !!stream.value)
|
||||
|
||||
const {
|
||||
init: initVAD,
|
||||
dispose: disposeVAD,
|
||||
start: startVAD,
|
||||
loaded: vadLoaded,
|
||||
} = useVAD(workletUrl, {
|
||||
threshold: ref(0.6),
|
||||
onSpeechStart: () => handleSpeechStart(),
|
||||
onSpeechEnd: () => handleSpeechEnd(),
|
||||
})
|
||||
|
||||
let stopOnStopRecord: (() => void) | undefined
|
||||
|
||||
async function startAudioInteraction() {
|
||||
try {
|
||||
await initVAD()
|
||||
if (stream.value)
|
||||
await startVAD(stream.value)
|
||||
|
||||
// Hook once
|
||||
stopOnStopRecord = onStopRecord(async (recording) => {
|
||||
const text = await transcribeForRecording(recording)
|
||||
if (!text || !text.trim())
|
||||
return
|
||||
|
||||
try {
|
||||
const provider = await providersStore.getProviderInstance(activeChatProvider.value)
|
||||
if (!provider || !activeChatModel.value)
|
||||
return
|
||||
|
||||
await chatStore.send(text, { model: activeChatModel.value, chatProvider: provider as ChatProvider })
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Failed to send chat from voice:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Audio interaction init failed:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSpeechStart() {
|
||||
if (shouldUseStreamInput.value && stream.value) {
|
||||
await transcribeForMediaStream(stream.value, {
|
||||
onSentenceEnd: (delta) => {
|
||||
const finalText = delta
|
||||
if (!finalText || !finalText.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const provider = await providersStore.getProviderInstance(activeChatProvider.value)
|
||||
if (!provider || !activeChatModel.value)
|
||||
return
|
||||
|
||||
await chatStore.send(finalText, { model: activeChatModel.value, chatProvider: provider as ChatProvider })
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Failed to send chat from voice:', err)
|
||||
}
|
||||
})()
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
startRecord()
|
||||
}
|
||||
|
||||
async function handleSpeechEnd() {
|
||||
if (shouldUseStreamInput.value) {
|
||||
// Keep streaming session alive; idle timer in pipeline will handle teardown.
|
||||
return
|
||||
}
|
||||
|
||||
stopRecord()
|
||||
}
|
||||
|
||||
function stopAudioInteraction() {
|
||||
try {
|
||||
stopOnStopRecord?.()
|
||||
stopOnStopRecord = undefined
|
||||
disposeVAD()
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
watch(enabled, async (val) => {
|
||||
if (val) {
|
||||
await startAudioInteraction()
|
||||
}
|
||||
else {
|
||||
stopAudioInteraction()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAudioInteraction()
|
||||
})
|
||||
|
||||
watch([stream, () => vadLoaded.value], async ([s, loaded]) => {
|
||||
if (enabled.value && loaded && s) {
|
||||
try {
|
||||
await startVAD(s)
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to start VAD with stream:', e)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BackgroundProvider
|
||||
ref="backgroundSurface"
|
||||
class="widgets top-widgets"
|
||||
:background="selectedOption"
|
||||
:top-color="sampledColor"
|
||||
>
|
||||
<div relative flex="~ col" z-2 h-100dvh w-100vw of-hidden>
|
||||
<!-- header -->
|
||||
<div class="px-0 py-1 md:px-3 md:py-3" w-full gap-2>
|
||||
<Header class="hidden md:flex" />
|
||||
<MobileHeader class="flex md:hidden" />
|
||||
</div>
|
||||
<!-- page -->
|
||||
<div relative flex="~ 1 row gap-y-0 gap-x-2 <md:col">
|
||||
<WidgetStage
|
||||
flex-1 min-w="1/2"
|
||||
:paused="paused"
|
||||
:focus-at="{
|
||||
x: positionCursor.x.value,
|
||||
y: positionCursor.y.value,
|
||||
}"
|
||||
:x-offset="`${isMobile ? position.x : position.x - 10}%`"
|
||||
:y-offset="positionInPercentageString.y"
|
||||
:scale="scale"
|
||||
/>
|
||||
<InteractiveArea v-if="!isMobile" h="85dvh" absolute right-4 flex flex-1 flex-col max-w="500px" min-w="30%" />
|
||||
<MobileInteractiveArea v-if="isMobile" @settings-open="handleSettingsOpen" />
|
||||
</div>
|
||||
</div>
|
||||
</BackgroundProvider>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
name: IndexScenePage
|
||||
meta:
|
||||
layout: stage
|
||||
stageTransition:
|
||||
name: bubble-wave-out
|
||||
</route>
|
||||
@@ -0,0 +1,145 @@
|
||||
<script setup lang="ts">
|
||||
import { CheckBar, IconItem } from '@proj-airi/stage-ui/components'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const settings = useSettings()
|
||||
|
||||
const menu = computed(() => [
|
||||
{
|
||||
title: 'Audio Record',
|
||||
description: 'Test Audio related composables',
|
||||
icon: 'i-solar:sledgehammer-bold-duotone',
|
||||
to: '/devtools/audio-record',
|
||||
},
|
||||
{
|
||||
title: 'Background Theme color blending',
|
||||
description: 'Test blending & theme',
|
||||
icon: 'i-solar:sledgehammer-bold-duotone',
|
||||
to: '/devtools/background-gradient-blending',
|
||||
},
|
||||
{
|
||||
title: 'Background removal (WebGPU required)',
|
||||
description: 'Utility for background removal',
|
||||
icon: 'i-solar:sledgehammer-bold-duotone',
|
||||
to: '/devtools/background-removal',
|
||||
},
|
||||
{
|
||||
title: 'Chat',
|
||||
description: 'Chat',
|
||||
icon: 'i-solar:sledgehammer-bold-duotone',
|
||||
to: '/devtools/chat',
|
||||
},
|
||||
{
|
||||
title: 'Gesture Circle (Desktop only)',
|
||||
description: 'Test gesture recognition',
|
||||
icon: 'i-solar:sledgehammer-bold-duotone',
|
||||
to: '/devtools/gesture-circle',
|
||||
},
|
||||
{
|
||||
title: 'Image',
|
||||
description: 'Image',
|
||||
icon: 'i-solar:sledgehammer-bold-duotone',
|
||||
to: '/devtools/image',
|
||||
},
|
||||
{
|
||||
title: 'Polaroid',
|
||||
description: 'Utility for taking shots of models',
|
||||
icon: 'i-solar:sledgehammer-bold-duotone',
|
||||
to: '/devtools/polaroid',
|
||||
},
|
||||
{
|
||||
title: t('settings.pages.system.sections.section.developer.sections.section.use-magic-keys.title'),
|
||||
description: t('settings.pages.system.sections.section.developer.sections.section.use-magic-keys.description'),
|
||||
icon: 'i-solar:sledgehammer-bold-duotone',
|
||||
to: '/devtools/use-magic-keys',
|
||||
},
|
||||
{
|
||||
title: 'Color extract',
|
||||
description: 'Test color extraction',
|
||||
icon: 'i-solar:sledgehammer-bold-duotone',
|
||||
to: '/devtools/vibrant',
|
||||
},
|
||||
{
|
||||
title: 'Aliyun Real-time Transcriber',
|
||||
description: 'Stream microphone audio to Aliyun NLS and inspect live transcripts',
|
||||
icon: 'i-solar:sledgehammer-bold-duotone',
|
||||
to: '/devtools/providers-transcription-realtime-aliyun-nls',
|
||||
},
|
||||
{
|
||||
title: 'Performance Playground',
|
||||
description: 'Test performance runtime: VRM expressions + TTS lip sync',
|
||||
icon: 'i-solar:sledgehammer-bold-duotone',
|
||||
to: '/devtools/performance-playground',
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CheckBar
|
||||
v-model="settings.disableTransitions"
|
||||
v-motion
|
||||
mb-2
|
||||
icon-on="i-solar:people-nearby-bold-duotone"
|
||||
icon-off="i-solar:running-2-line-duotone"
|
||||
text="settings.animations.stage-transitions.title"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (19 * 10)"
|
||||
:delay="1 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
/>
|
||||
<CheckBar
|
||||
v-model="settings.usePageSpecificTransitions"
|
||||
v-motion
|
||||
:disabled="settings.disableTransitions"
|
||||
icon-on="i-solar:running-2-line-duotone"
|
||||
icon-off="i-solar:people-nearby-bold-duotone"
|
||||
text="settings.animations.use-page-specific-transitions.title"
|
||||
description="settings.animations.use-page-specific-transitions.description"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (20 * 10)"
|
||||
:delay="2 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
/>
|
||||
|
||||
<div flex="~ col gap-4" pb-12>
|
||||
<IconItem
|
||||
v-for="(item, index) in menu"
|
||||
:key="item.to"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:style="{
|
||||
transitionDelay: `${index * 50}ms`, // delay between each item, unocss doesn't support dynamic generation of classes now
|
||||
}"
|
||||
:title="item.title"
|
||||
:description="item.description"
|
||||
:icon="item.icon"
|
||||
:to="item.to"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[65dvh]" right--15 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, rotate: 30 }"
|
||||
:enter="{ scale: 1, opacity: 1, rotate: 0 }"
|
||||
:duration="250"
|
||||
flex items-center justify-center
|
||||
>
|
||||
<div text="60" i-solar:code-bold-duotone />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { IconItem, RippleGrid } from '@proj-airi/stage-ui/components'
|
||||
import { useRippleGridState } from '@proj-airi/stage-ui/composables/use-ripple-grid-state'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { lastClickedIndex, setLastClickedIndex } = useRippleGridState()
|
||||
|
||||
const settings = computed(() => [
|
||||
{
|
||||
title: t('settings.pages.system.general.title'),
|
||||
description: t('settings.pages.system.general.description'),
|
||||
icon: 'i-solar:emoji-funny-square-bold-duotone',
|
||||
to: '/settings/system/general',
|
||||
},
|
||||
{
|
||||
title: t('settings.pages.system.color-scheme.title'),
|
||||
description: t('settings.pages.system.color-scheme.description'),
|
||||
icon: 'i-solar:pallete-2-bold-duotone',
|
||||
to: '/settings/system/color-scheme',
|
||||
},
|
||||
{
|
||||
title: t('settings.pages.system.developer.title'),
|
||||
description: t('settings.pages.system.developer.description'),
|
||||
icon: 'i-solar:code-bold-duotone',
|
||||
to: '/settings/system/developer',
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ col gap-4" font-normal>
|
||||
<div />
|
||||
<div flex="~ col gap-4">
|
||||
<RippleGrid
|
||||
:items="settings"
|
||||
:get-key="item => item.to"
|
||||
:columns="1"
|
||||
:origin-index="lastClickedIndex"
|
||||
@item-click="({ globalIndex }) => setLastClickedIndex(globalIndex)"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<IconItem
|
||||
:title="item.title"
|
||||
:description="item.description"
|
||||
:icon="item.icon"
|
||||
:to="item.to"
|
||||
/>
|
||||
</template>
|
||||
</RippleGrid>
|
||||
</div>
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[calc(100dvh-12rem)]" bottom-0 right--10 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, rotate: 180 }"
|
||||
:enter="{ scale: 1, opacity: 1, rotate: 0 }"
|
||||
:duration="500"
|
||||
size-60
|
||||
flex items-center justify-center
|
||||
>
|
||||
<div v-motion text="60" i-solar:settings-bold-duotone />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
stageTransition:
|
||||
name: slide
|
||||
pageSpecificAvailable: true
|
||||
</route>
|
||||
@@ -0,0 +1,279 @@
|
||||
import type { BackgroundOption } from '@proj-airi/stage-ui/components'
|
||||
import type { Ref, ShallowRef } from 'vue'
|
||||
|
||||
import localforage from 'localforage'
|
||||
|
||||
import { useLocalStorage, useObjectUrl } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, markRaw, onScopeDispose, ref, shallowRef } from 'vue'
|
||||
|
||||
import { DefaultBackgroundPreview } from '../components/Backgrounds/default'
|
||||
|
||||
export enum BackgroundKind {
|
||||
Wave = 'wave',
|
||||
Image = 'image',
|
||||
}
|
||||
|
||||
export interface BackgroundItem extends BackgroundOption {
|
||||
kind: BackgroundKind
|
||||
importedAt?: number
|
||||
}
|
||||
|
||||
type PersistedBackgroundItem = Omit<BackgroundItem, 'file'> & {
|
||||
file?: Blob
|
||||
}
|
||||
|
||||
export const useBackgroundStore = defineStore('background', () => {
|
||||
// TODO: STORAGE_PREFIX used with multiple less maintainable `localforage` and `key.startsWith(...)` call that creates complexity.
|
||||
const STORAGE_PREFIX = 'background-'
|
||||
const presets: BackgroundItem[] = [
|
||||
{
|
||||
id: 'colorful-wave',
|
||||
label: 'Colorful Wave',
|
||||
description: 'Animated wave on cross grid',
|
||||
kind: BackgroundKind.Wave,
|
||||
component: markRaw(DefaultBackgroundPreview),
|
||||
},
|
||||
]
|
||||
|
||||
const options = ref<BackgroundItem[]>([...presets])
|
||||
const loading = ref(false)
|
||||
|
||||
const selectedId = useLocalStorage<string>('settings/theme/background/selected-id', options.value[0]?.id)
|
||||
const sampledColor = useLocalStorage<string>('settings/theme/background/sampled-color', '')
|
||||
|
||||
const selectedOption = computed(() => options.value.find(option => option.id === selectedId.value) ?? options.value[0])
|
||||
|
||||
const blobRefs = new Map<string, ShallowRef<Blob | undefined>>()
|
||||
const urlRefs = new Map<string, Readonly<Ref<string | undefined>>>()
|
||||
|
||||
function ensureObjectUrl(id: string, blob: Blob) {
|
||||
let blobRef = blobRefs.get(id)
|
||||
let urlRef = urlRefs.get(id)
|
||||
|
||||
if (!blobRef || !urlRef) {
|
||||
blobRef = shallowRef<Blob | undefined>(blob)
|
||||
blobRefs.set(id, blobRef)
|
||||
urlRef = useObjectUrl(blobRef)
|
||||
urlRefs.set(id, urlRef)
|
||||
}
|
||||
|
||||
if (blobRef.value !== blob)
|
||||
blobRef.value = blob
|
||||
|
||||
return urlRef!.value!
|
||||
}
|
||||
|
||||
onScopeDispose(() => {
|
||||
blobRefs.clear()
|
||||
urlRefs.clear()
|
||||
})
|
||||
|
||||
async function migrateDataUrlToBlob(key: string, val: PersistedBackgroundItem, dataUrl: string) {
|
||||
try {
|
||||
const blob = await (await fetch(dataUrl)).blob()
|
||||
const objectUrl = ensureObjectUrl(key, blob)
|
||||
|
||||
const existing = options.value.find(o => o.id === key)
|
||||
if (existing) {
|
||||
existing.src = objectUrl
|
||||
existing.file = undefined
|
||||
}
|
||||
|
||||
const payload: PersistedBackgroundItem = {
|
||||
...val,
|
||||
src: undefined,
|
||||
file: blob,
|
||||
}
|
||||
|
||||
await localforage.setItem<PersistedBackgroundItem>(key, payload)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to migrate background data URL to Blob', error)
|
||||
}
|
||||
}
|
||||
|
||||
function setSelection(option: BackgroundItem, color?: string) {
|
||||
selectedId.value = option.id
|
||||
if (color)
|
||||
sampledColor.value = color
|
||||
}
|
||||
|
||||
async function applyPickerSelection(payload: { option: BackgroundOption, color?: string }) {
|
||||
const kind = payload.option.kind === BackgroundKind.Wave
|
||||
? BackgroundKind.Wave
|
||||
: payload.option.kind === BackgroundKind.Image
|
||||
? BackgroundKind.Image
|
||||
: BackgroundKind.Image
|
||||
|
||||
const selection: BackgroundItem = {
|
||||
...payload.option,
|
||||
kind,
|
||||
}
|
||||
|
||||
const saved = await addOption(selection)
|
||||
setSelection(saved, payload.color)
|
||||
return saved
|
||||
}
|
||||
|
||||
async function loadFromIndexedDb() {
|
||||
if (loading.value)
|
||||
return
|
||||
loading.value = true
|
||||
const stored: BackgroundItem[] = []
|
||||
try {
|
||||
await localforage.iterate<PersistedBackgroundItem, void>((val, key) => {
|
||||
if (!key.startsWith(STORAGE_PREFIX))
|
||||
return
|
||||
|
||||
const storedBlob = val.file instanceof Blob ? val.file : undefined
|
||||
const storedSrc = typeof val.src === 'string' && val.src.length > 0 ? val.src : undefined
|
||||
|
||||
if (storedBlob) {
|
||||
const objectUrl = ensureObjectUrl(key, storedBlob)
|
||||
stored.push({
|
||||
...val,
|
||||
id: key,
|
||||
kind: BackgroundKind.Image,
|
||||
src: objectUrl,
|
||||
file: undefined,
|
||||
component: undefined,
|
||||
removable: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (storedSrc) {
|
||||
stored.push({
|
||||
...val,
|
||||
id: key,
|
||||
kind: BackgroundKind.Image,
|
||||
src: storedSrc,
|
||||
file: undefined,
|
||||
component: undefined,
|
||||
removable: true,
|
||||
})
|
||||
|
||||
if (storedSrc.startsWith('data:')) {
|
||||
setTimeout(() => {
|
||||
void migrateDataUrlToBlob(key, val, storedSrc)
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to load backgrounds from IndexedDB', error)
|
||||
}
|
||||
|
||||
options.value = [...presets, ...stored].sort((a, b) => (b.importedAt ?? 0) - (a.importedAt ?? 0))
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
void loadFromIndexedDb()
|
||||
|
||||
async function addOption(option: BackgroundItem): Promise<BackgroundItem> {
|
||||
const normalizedId = option.file ? (option.id.startsWith(STORAGE_PREFIX) ? option.id : `${STORAGE_PREFIX}${option.id}`) : option.id
|
||||
|
||||
const hasUploadedFile = option.file instanceof Blob
|
||||
const storedBlob = hasUploadedFile ? option.file : undefined
|
||||
|
||||
const src = storedBlob
|
||||
? ensureObjectUrl(normalizedId, storedBlob)
|
||||
: option.src
|
||||
|
||||
const normalizedOption: BackgroundItem = {
|
||||
...option,
|
||||
id: normalizedId,
|
||||
kind: option.kind ?? BackgroundKind.Image,
|
||||
component: option.component ? markRaw(option.component) : option.component,
|
||||
src,
|
||||
importedAt: option.importedAt ?? Date.now(),
|
||||
blur: option.blur,
|
||||
file: undefined,
|
||||
removable: true,
|
||||
}
|
||||
|
||||
const existing = options.value.find(o => o.id === normalizedId)
|
||||
if (existing) {
|
||||
Object.assign(existing, normalizedOption)
|
||||
}
|
||||
else {
|
||||
options.value.push(normalizedOption)
|
||||
}
|
||||
selectedId.value = normalizedId
|
||||
|
||||
if (hasUploadedFile && storedBlob) {
|
||||
const payload: PersistedBackgroundItem = {
|
||||
...normalizedOption,
|
||||
// ensure we store under prefix for consistency
|
||||
id: normalizedId.startsWith(STORAGE_PREFIX) ? normalizedId : `${STORAGE_PREFIX}${normalizedId}`,
|
||||
src: undefined,
|
||||
file: storedBlob,
|
||||
removable: true,
|
||||
}
|
||||
try {
|
||||
await localforage.setItem<PersistedBackgroundItem>(payload.id, payload)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to persist background', error)
|
||||
}
|
||||
}
|
||||
|
||||
return normalizedOption
|
||||
}
|
||||
|
||||
async function removeOption(optionId: string) {
|
||||
const optionIndex = options.value.findIndex(o => o.id === optionId)
|
||||
if (optionIndex === -1)
|
||||
return
|
||||
|
||||
const option = options.value[optionIndex]
|
||||
|
||||
// Remove from localforage
|
||||
try {
|
||||
if (option.id.startsWith(STORAGE_PREFIX)) {
|
||||
await localforage.removeItem(option.id)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to remove background from storage', error)
|
||||
}
|
||||
|
||||
const blobRef = blobRefs.get(optionId)
|
||||
if (blobRef)
|
||||
blobRef.value = undefined
|
||||
blobRefs.delete(optionId)
|
||||
urlRefs.delete(optionId)
|
||||
|
||||
// Remove from list
|
||||
options.value.splice(optionIndex, 1)
|
||||
|
||||
// If selected, fallback to first available option
|
||||
if (selectedId.value === optionId) {
|
||||
const fallback = options.value[0]
|
||||
if (fallback) {
|
||||
selectedId.value = fallback.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setSampledColor(color?: string) {
|
||||
if (color)
|
||||
sampledColor.value = color
|
||||
}
|
||||
|
||||
return {
|
||||
options,
|
||||
selectedId,
|
||||
selectedOption,
|
||||
sampledColor,
|
||||
loading,
|
||||
loadFromIndexedDb,
|
||||
addOption,
|
||||
removeOption,
|
||||
setSelection,
|
||||
applyPickerSelection,
|
||||
setSampledColor,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
@import './transitions.css';
|
||||
@import './vue-transitions.css';
|
||||
|
||||
:root {
|
||||
--bg-color-light: rgb(255 255 255);
|
||||
--bg-color-dark: rgb(18 18 18);
|
||||
--bg-color: var(--bg-color-light);
|
||||
}
|
||||
|
||||
/**
|
||||
Disable double-tap "zoom" option in browser on touch devices
|
||||
|
||||
https://stackoverflow.com/a/54207844
|
||||
https://stackoverflow.com/questions/10614481/disable-double-tap-zoom-option-in-browser-on-touch-devices
|
||||
*/
|
||||
* {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
html {
|
||||
background: var(--bg-color);
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
--bg-color: var(--bg-color-dark);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
#nprogress {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
background: color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 70%, oklch(50% 0 360));
|
||||
opacity: 0.75;
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.dark #nprogress .bar {
|
||||
background: `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 90%, oklch(90% 0 360))`;
|
||||
}
|
||||
|
||||
@media (display-mode: standalone) {
|
||||
#nprogress {
|
||||
display: none;
|
||||
}
|
||||
#nprogress .bar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
.slide-away-enter-active,
|
||||
.slide-away-leave-active {
|
||||
transition:
|
||||
transform 0.3s ease-in-out,
|
||||
opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.slide-away-enter,
|
||||
.slide-away-leave-to {
|
||||
transform: translateY(-10px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-away-enter-from,
|
||||
.slide-away-leave {
|
||||
transform: translateY(10px);
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
.fade-slide-out-r-to-l-enter-active,
|
||||
.fade-slide-out-r-to-l-leave-active {
|
||||
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-slide-out-r-to-l-enter-from,
|
||||
.fade-slide-out-r-to-l-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(10px);
|
||||
}
|
||||
|
||||
.fade-slide-out-r-to-l-enter-to,
|
||||
.fade-slide-out-r-to-l-leave-from {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.fade-slide-out-l-to-r-enter-active,
|
||||
.fade-slide-out-l-to-r-leave-active {
|
||||
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-slide-out-l-to-r-enter-from,
|
||||
.fade-slide-out-l-to-r-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
|
||||
.fade-slide-out-l-to-r-enter-to,
|
||||
.fade-slide-out-l-to-r-leave-from {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-enter-to,
|
||||
.fade-leave-from {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { CommonRequestOptions } from '@xsai/shared'
|
||||
import type { Message } from '@xsai/shared-chat'
|
||||
import type { Infer, Schema } from 'xsschema'
|
||||
|
||||
import { generateText } from '@xsai/generate-text'
|
||||
import { message } from '@xsai/utils-chat'
|
||||
import { toJsonSchema, validate } from 'xsschema'
|
||||
|
||||
type SchemaOrString<S extends Schema | undefined | unknown> = S extends unknown ? string : S extends Schema ? Infer<S> : never
|
||||
|
||||
async function parseJSONFormat<S extends Schema, R extends SchemaOrString<S>>(content: string, options: { messages: Message[], apiKey?: string, baseURL: string, model: string } & Partial<CommonRequestOptions>, schema?: S, erroredValue?: string, errorMessage?: string): Promise<R> {
|
||||
if (!schema)
|
||||
return content as unknown as R
|
||||
|
||||
try {
|
||||
let parsedContent: Infer<S>
|
||||
let correctionPrompt = ''
|
||||
|
||||
if (erroredValue && errorMessage) {
|
||||
correctionPrompt = `Previous response "${JSON.stringify(erroredValue)}" was invalid due to: ${JSON.stringify(errorMessage)}\n\n`
|
||||
}
|
||||
|
||||
try {
|
||||
parsedContent = JSON.parse(content)
|
||||
}
|
||||
catch (parseError) {
|
||||
console.error('Error parsing JSON:', parseError, content)
|
||||
|
||||
options.messages.push(message.user(`
|
||||
${correctionPrompt}The response was not valid JSON:
|
||||
${JSON.stringify(content)}
|
||||
|
||||
Error: ${String(parseError)}
|
||||
|
||||
Please provide a corrected JSON response that matches the schema:
|
||||
${JSON.stringify(await toJsonSchema(schema))}`))
|
||||
|
||||
const response = await call(options, schema)
|
||||
return parseJSONFormat(response, options, schema, content, String(parseError))
|
||||
}
|
||||
|
||||
// TODO: print validation issues
|
||||
return await validate(schema, parsedContent) as R
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error processing response:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes user input and generates LLM response along with thought nodes.
|
||||
*/
|
||||
async function call<S extends Schema, R extends SchemaOrString<S>>(options: { messages: Message[], apiKey?: string, baseURL: string, model: string } & Partial<CommonRequestOptions>, schema?: S): Promise<R> {
|
||||
if (schema != null) {
|
||||
options.messages.push(message.user(`Your response must follow the following schema:
|
||||
${JSON.stringify(await toJsonSchema(schema))}
|
||||
|
||||
Without any extra markups such as \`\`\` in markdown, or descriptions.`))
|
||||
}
|
||||
|
||||
const response = await generateText({
|
||||
baseURL: options.baseURL,
|
||||
apiKey: options.apiKey,
|
||||
model: options.model,
|
||||
messages: options.messages,
|
||||
})
|
||||
|
||||
return await parseJSONFormat<S, R>(response.text || '', options, schema)
|
||||
}
|
||||
|
||||
export async function generateObject<S extends Schema, R extends SchemaOrString<S>>(options: { messages: Message[], model: string, apiKey?: string, baseURL: string } & Partial<CommonRequestOptions>, schema?: S): Promise<R> {
|
||||
return await call(options, schema)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export function isPlatformTamagotchi() {
|
||||
return import.meta.env.MODE === 'tamagotchi'
|
||||
}
|
||||
|
||||
export function isStandalone() {
|
||||
if (import.meta.env.SSR) {
|
||||
return false
|
||||
}
|
||||
if (!('window' in globalThis) || globalThis.window == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const mediaStandalone = ('matchMedia' in window) && window.matchMedia('(display-mode: standalone)').matches
|
||||
const navigatorStandalone = ('navigator' in window && 'standalone' in window.navigator && window.navigator.standalone)
|
||||
const referrerStandalone = document.referrer.includes('android-app://')
|
||||
|
||||
return mediaStandalone || navigatorStandalone || referrerStandalone
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { createVADStates } from './manager'
|
||||
export type { VADAudioOptions } from './manager'
|
||||
export { createVAD, VAD } from './vad'
|
||||
@@ -0,0 +1,2 @@
|
||||
export type { VADAudioOptions } from '@proj-airi/stage-ui/libs/audio/vad'
|
||||
export { createVADStates } from '@proj-airi/stage-ui/libs/audio/vad'
|
||||
@@ -0,0 +1,53 @@
|
||||
// vad-worklet-processor.ts
|
||||
// This file needs to be registered as an AudioWorklet
|
||||
|
||||
/**
|
||||
* Minimum chunk size for processing audio
|
||||
*/
|
||||
const MIN_CHUNK_SIZE = 512
|
||||
|
||||
/**
|
||||
* Global state for audio buffer accumulation
|
||||
*/
|
||||
let globalPointer = 0
|
||||
const globalBuffer = new Float32Array(MIN_CHUNK_SIZE)
|
||||
|
||||
/**
|
||||
* VAD AudioWorklet Processor - processes audio chunks and sends them to the main thread
|
||||
*/
|
||||
class VADProcessor extends AudioWorkletProcessor {
|
||||
process(inputs: Float32Array[][], _outputs: Float32Array[][], _parameters: Record<string, Float32Array>) {
|
||||
const buffer = inputs[0][0]
|
||||
if (!buffer)
|
||||
return true // buffer is null when the stream ends
|
||||
|
||||
if (buffer.length > MIN_CHUNK_SIZE) {
|
||||
// If the buffer is larger than the minimum chunk size, send the entire buffer
|
||||
this.port.postMessage({ buffer })
|
||||
}
|
||||
else {
|
||||
const remaining = MIN_CHUNK_SIZE - globalPointer
|
||||
if (buffer.length >= remaining) {
|
||||
// If the buffer is larger than (or equal to) the remaining space in the global buffer, copy the remaining space
|
||||
globalBuffer.set(buffer.subarray(0, remaining), globalPointer)
|
||||
|
||||
// Send the global buffer
|
||||
this.port.postMessage({ buffer: globalBuffer })
|
||||
|
||||
// Reset the global buffer and set the remaining buffer
|
||||
globalBuffer.fill(0)
|
||||
globalBuffer.set(buffer.subarray(remaining), 0)
|
||||
globalPointer = buffer.length - remaining
|
||||
}
|
||||
else {
|
||||
// If the buffer is smaller than the remaining space in the global buffer, copy the buffer to the global buffer
|
||||
globalBuffer.set(buffer, globalPointer)
|
||||
globalPointer += buffer.length
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('vad-audio-worklet-processor', VADProcessor)
|
||||