ai voice chat edit

This commit is contained in:
Mohamad Mahdi Jebeli 2026-07-18 15:19:21 +03:30
parent 7ca14108b9
commit 008baa71f9
13 changed files with 2539 additions and 246 deletions

4
.gitignore vendored
View File

@ -1,6 +1,10 @@
android/app/.cxx
android/app/.cxx/Debug
# Graphify generated output
/graphify-out/
# Miscellaneous
*.class
*.log

View File

@ -3,10 +3,12 @@ package com.didvan.didvanapp
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.media.AudioManager
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.view.KeyEvent
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
@ -17,11 +19,37 @@ import java.io.FileInputStream
class MainActivity: FlutterActivity() {
private val CHANNEL = "com.didvan.shareFile"
private val AUDIO_EVENTS_CHANNEL = "com.didvan.didvanapp/audio_events"
private var audioEventsChannel: MethodChannel? = null
private var outputProbePreviousVolume: Int? = null
@Override
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
audioEventsChannel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
AUDIO_EVENTS_CHANNEL
)
audioEventsChannel?.setMethodCallHandler { call, result ->
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
when (call.method) {
"beginOutputProbe" -> {
if (outputProbePreviousVolume == null) {
outputProbePreviousVolume =
audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
}
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 0, 0)
result.success(null)
}
"endOutputProbe" -> {
restoreOutputProbeVolume(audioManager)
result.success(null)
}
else -> result.notImplemented()
}
}
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"copyToDownloads" -> {
@ -45,6 +73,42 @@ class MainActivity: FlutterActivity() {
}
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (
event.keyCode == KeyEvent.KEYCODE_VOLUME_UP ||
event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
event.keyCode == KeyEvent.KEYCODE_VOLUME_MUTE
) {
// Do not consume the key: Android must still change the volume.
// One physical press produces DOWN, repeat and UP events. Notify
// Dart only once so a held key cannot continuously restart its
// acoustic-path guard.
if (event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) {
audioEventsChannel?.invokeMethod(
"volumeChanged",
mapOf(
"keyCode" to event.keyCode
)
)
}
}
return super.dispatchKeyEvent(event)
}
override fun onDestroy() {
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
restoreOutputProbeVolume(audioManager)
audioEventsChannel?.setMethodCallHandler(null)
audioEventsChannel = null
super.onDestroy()
}
private fun restoreOutputProbeVolume(audioManager: AudioManager) {
val previousVolume = outputProbePreviousVolume ?: return
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, previousVolume, 0)
outputProbePreviousVolume = null
}
private fun copyFileToDownloads(filePath: String, fileName: String): Boolean {
return try {
val context: Context = applicationContext

View File

@ -1,16 +1,3 @@
buildscript {
ext.kotlin_version = '1.9.0' // (ممکن است ورژن کاتلین شما متفاوت باشد، به آن دست نزنید)
repositories {
google() // حتماً خط اول باشد
mavenCentral() // حتماً خط دوم باشد
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.0' // (ورژن گریدل شما)
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google() // حتماً خط اول باشد

296
lib/shaders/ai_orb_3d.frag Normal file
View File

@ -0,0 +1,296 @@
#include <flutter/runtime_effect.glsl>
uniform vec2 uResolution;
uniform float uTime;
uniform float uActivity;
uniform vec3 uAccent;
uniform float uTransient;
uniform float uTexture;
uniform float uMotionPhase;
out vec4 fragColor;
const float PI = 3.14159265359;
float saturate(float value) {
return clamp(value, 0.0, 1.0);
}
float hash21(vec2 point) {
point = fract(point * vec2(123.34, 456.21));
point += dot(point, point + 45.32);
return fract(point.x * point.y);
}
float softNoise(vec2 point) {
vec2 cell = floor(point);
vec2 local = fract(point);
local = local * local * (3.0 - 2.0 * local);
return mix(
mix(hash21(cell), hash21(cell + vec2(1.0, 0.0)), local.x),
mix(hash21(cell + vec2(0.0, 1.0)), hash21(cell + 1.0), local.x),
local.y
);
}
float softBox(vec2 point, vec2 halfSize, float feather) {
vec2 distance = abs(point) - halfSize;
return 1.0 - smoothstep(
0.0,
feather,
max(distance.x, distance.y)
);
}
float directionalLobe(vec2 radial, vec2 direction, float sharpness) {
return pow(saturate(dot(radial, normalize(direction))), sharpness);
}
vec3 studioEnvironment(vec3 direction) {
float skyMix = smoothstep(-0.55, 0.72, direction.y);
vec3 floorColor = vec3(0.012, 0.016, 0.028);
vec3 skyColor = vec3(0.36, 0.46, 0.68);
vec3 environmentColor = mix(floorColor, skyColor, skyMix);
float horizonStrip = pow(
saturate(1.0 - abs(direction.y + 0.09) * 4.8),
10.0
);
environmentColor += vec3(0.68, 0.78, 1.0) * horizonStrip * 0.42;
float keyPanel = pow(
saturate(dot(direction, normalize(vec3(-0.69, -0.28, 0.67)))),
24.0
);
environmentColor += vec3(0.93, 0.96, 1.0) * keyPanel * 0.72;
float rimPanel = pow(
saturate(dot(direction, normalize(vec3(0.76, 0.04, 0.65)))),
34.0
);
environmentColor += vec3(0.34, 0.52, 1.0) * rimPanel * 0.48;
float warmPanel = pow(
saturate(dot(direction, normalize(vec3(0.42, 0.58, 0.69)))),
42.0
);
environmentColor += vec3(0.76, 0.49, 0.31) * warmPanel * 0.16;
return environmentColor;
}
void main() {
vec2 fragCoord = FlutterFragCoord().xy;
vec2 centered = fragCoord - uResolution * 0.5;
vec2 uv = centered / min(uResolution.x, uResolution.y);
float activity = saturate(uActivity);
float transientEnergy = saturate(uTransient);
float textureEnergy = saturate(uTexture);
float timePhase = uTime * PI * 2.0;
float voiceEnergy = smoothstep(0.045, 0.88, activity);
float radius = 0.326 + voiceEnergy * 0.0015;
float distanceToCenter = length(uv);
float safeDistance = max(distanceToCenter, 0.0001);
vec2 radial = uv / safeDistance;
float distanceFromSurface = distanceToCenter - radius;
// Reflections around the silhouette are directional rather than a uniform
// neon ring. They read as large studio lights caught by a glass surface.
float perimeterBand = exp(-abs(distanceFromSurface) * 72.0);
float upperLeftArc = directionalLobe(
radial,
vec2(-0.72, -0.69),
8.0
);
float rightArc = directionalLobe(radial, vec2(0.96, -0.12), 13.0);
float lowerArc = directionalLobe(radial, vec2(0.28, 0.96), 18.0);
float arcBreath = 0.92 + 0.08 * sin(
uMotionPhase * 0.72 + textureEnergy * 1.8
);
vec3 perimeterReflection =
vec3(0.88, 0.93, 1.0) * upperLeftArc * 0.38 +
mix(uAccent, vec3(0.48, 0.68, 1.0), 0.48) * rightArc * 0.3 +
vec3(0.95, 0.58, 0.38) * lowerArc * 0.075;
perimeterReflection *= perimeterBand * arcBreath;
float halo = exp(-max(distanceFromSurface, 0.0) * 15.0);
halo *= 1.0 - smoothstep(radius, radius * 2.42, distanceToCenter);
vec3 haloColor = uAccent * halo * (
0.025 + voiceEnergy * 0.055 + transientEnergy * 0.025
);
// A soft contact shadow and a dim, compressed floor reflection ground the
// sphere in space. Voice changes the reflection, not its physical position.
vec2 shadowUv = vec2(
uv.x / (radius * 1.08),
(uv.y - radius * 1.12) / (radius * 0.18)
);
float contactShadow = exp(
-(shadowUv.x * shadowUv.x * 2.3 + shadowUv.y * shadowUv.y * 1.55)
);
float reflectionBreakup = 0.72 + 0.28 * sin(
shadowUv.x * 7.0 + uMotionPhase * 0.36
);
float floorReflection = contactShadow * reflectionBreakup *
(0.12 + voiceEnergy * 0.1);
vec3 outsideColor =
haloColor +
perimeterReflection +
uAccent * floorReflection * 0.16 +
vec3(0.018, 0.022, 0.036) * contactShadow * 0.62;
float outsideAlpha = saturate(
halo * 0.17 +
perimeterBand * (upperLeftArc * 0.42 + rightArc * 0.34) +
contactShadow * 0.34
);
if (distanceToCenter > radius) {
fragColor = vec4(outsideColor, outsideAlpha);
return;
}
float z = sqrt(max(radius * radius - dot(uv, uv), 0.0));
vec3 geometricNormal = normalize(vec3(uv / radius, z / radius));
// Microscopic normal variation gives the material thickness without
// visibly deforming the sphere. Speech texture subtly increases it.
vec2 noiseCoordinate =
geometricNormal.xy * 5.1 +
vec2(timePhase * 0.025, -timePhase * 0.018);
float noiseA = softNoise(noiseCoordinate);
float noiseB = softNoise(noiseCoordinate * 1.73 + vec2(2.7, -1.9));
vec2 microNormal = vec2(noiseA - 0.5, noiseB - 0.5) *
(0.0035 + textureEnergy * 0.009);
vec3 normal = normalize(vec3(
geometricNormal.xy + microNormal,
geometricNormal.z
));
vec3 viewDirection = vec3(0.0, 0.0, 1.0);
vec3 keyDirection = normalize(vec3(
-0.48 + sin(uMotionPhase * 0.31) * voiceEnergy * 0.012,
-0.63 + cos(uMotionPhase * 0.27) * voiceEnergy * 0.009,
0.88
));
vec3 fillDirection = normalize(vec3(0.76, 0.16, 0.54));
float diffuse = max(dot(normal, keyDirection), 0.0);
float fill = max(dot(normal, fillDirection), 0.0);
float ndv = saturate(dot(normal, viewDirection));
float fresnel = 0.035 + 0.965 * pow(1.0 - ndv, 5.0);
float thickness = (2.0 * z) / radius;
vec3 reflectedDirection = normalize(reflect(-viewDirection, normal));
vec3 reflection = studioEnvironment(reflectedDirection);
// Three close indices of refraction create restrained chromatic dispersion
// at the edge, a key cue for real optical glass.
vec3 refractedRed = studioEnvironment(normalize(
refract(-viewDirection, normal, 0.695)
));
vec3 refractedGreen = studioEnvironment(normalize(
refract(-viewDirection, normal, 0.715)
));
vec3 refractedBlue = studioEnvironment(normalize(
refract(-viewDirection, normal, 0.738)
));
vec3 refractedEnvironment = vec3(
refractedRed.r,
refractedGreen.g,
refractedBlue.b
);
vec3 absorption = vec3(0.19, 0.13, 0.075);
vec3 transmission = exp(-absorption * thickness);
vec3 deepTint = mix(vec3(0.008, 0.012, 0.024), uAccent * 0.12, 0.38);
vec3 color = deepTint * (0.2 + diffuse * 0.16 + fill * 0.04);
color += refractedEnvironment * transmission * (0.15 + ndv * 0.08);
color += reflection * (0.22 + fresnel * 1.04);
color += uAccent * fresnel * (0.035 + voiceEnergy * 0.065);
// Coherent rectangular reflections sell the object as a glossy 3D sphere.
float surfaceVisibility = smoothstep(0.12, 0.7, geometricNormal.z);
float keySoftbox = softBox(
normal.xy - vec2(-0.34, -0.3),
vec2(0.055, 0.235),
0.055
) * surfaceVisibility;
float topSoftbox = softBox(
normal.xy - vec2(0.05, -0.49),
vec2(0.27, 0.035),
0.06
) * surfaceVisibility;
float sideSoftbox = softBox(
normal.xy - vec2(0.56, 0.03),
vec2(0.026, 0.19),
0.048
) * surfaceVisibility;
color += vec3(0.91, 0.95, 1.0) * keySoftbox * 0.19;
color += vec3(0.72, 0.82, 1.0) * topSoftbox * 0.08;
color += mix(uAccent, vec3(0.5, 0.7, 1.0), 0.52) *
sideSoftbox * 0.13;
vec3 halfVector = normalize(keyDirection + viewDirection);
float sharpSpecular = pow(max(dot(normal, halfVector), 0.0), 180.0);
color += vec3(1.0, 0.98, 0.95) * sharpSpecular * 1.55;
vec3 broadHalf = normalize(vec3(-0.58, -0.4, 0.72) + viewDirection);
float broadSpecular = pow(max(dot(normal, broadHalf), 0.0), 25.0);
color += vec3(0.72, 0.82, 1.0) * broadSpecular * 0.1;
// Keep the optical center dark and clean. Speech travels through the glass
// as thin refracted sheets instead of an artificial glowing blob.
vec2 normalizedUv = uv / radius;
float internalDepth =
1.0 - smoothstep(0.12, 0.98, length(normalizedUv));
float cleanCenter = pow(internalDepth, 1.65);
color *= 1.0 - cleanCenter * 0.115;
float voicePulse = 0.82 + 0.18 * sin(
uMotionPhase * 1.23 + textureEnergy * 1.6
);
float primarySheetY =
normalizedUv.y +
normalizedUv.x * 0.11 +
0.13 -
sin(uMotionPhase * 0.54) * voiceEnergy * 0.055;
float primarySheet = exp(-primarySheetY * primarySheetY * 82.0) *
exp(-dot(normalizedUv, normalizedUv) * 1.15);
float secondarySheetY =
normalizedUv.y -
normalizedUv.x * 0.07 -
0.42 +
cos(uMotionPhase * 0.39) * voiceEnergy * 0.035;
float secondarySheet = exp(
-secondarySheetY * secondarySheetY * 115.0
) * exp(-dot(normalizedUv, normalizedUv) * 1.5);
color += mix(uAccent, vec3(0.66, 0.8, 1.0), 0.38) *
primarySheet * voiceEnergy * voicePulse * 0.052;
color += vec3(0.68, 0.8, 1.0) * secondarySheet *
voiceEnergy * 0.026;
float onsetFlash = pow(1.0 - ndv, 2.2) *
upperLeftArc * transientEnergy;
color += vec3(0.86, 0.92, 1.0) * onsetFlash * 0.18;
float causticPhase =
normalizedUv.x * 14.0 +
normalizedUv.y * 9.0 -
uMotionPhase * (0.52 + textureEnergy * 0.38) +
noiseA * 2.4;
float caustic = pow(saturate(0.5 + 0.5 * sin(causticPhase)), 9.0);
float causticBand = smoothstep(0.24, 0.52, length(normalizedUv)) *
(1.0 - smoothstep(0.78, 0.97, length(normalizedUv)));
caustic *= causticBand * voiceEnergy * (0.25 + textureEnergy * 0.75);
color += mix(uAccent, vec3(0.78, 0.88, 1.0), 0.58) *
caustic * 0.045;
// Bring the outside light sources continuously across the glass boundary.
color += perimeterReflection * (0.56 + fresnel * 0.64);
color *= mix(0.88, 1.025, geometricNormal.z);
float edgeAlpha = 1.0 - smoothstep(
radius * 0.982,
radius,
distanceToCenter
);
fragColor = vec4(color, edgeAlpha);
}

View File

@ -1109,7 +1109,18 @@ class _AiChatPageState extends State<AiChatPage> with TickerProviderStateMixin {
)
: Column(
children: [
if (file != null)
if (file != null && file.path.isEmpty)
// پیامهای قدیمی که تولیدشان ناموفق بود file
// خالی دارند پخش/دانلود ممکن نیست.
const Padding(
padding: EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
child: DidvanText(
'فایل در دسترس نیست. لطفاً دوباره تولید کنید.',
fontSize: 12,
),
)
else if (file != null)
(file.isAudio())
? Padding(
padding: const EdgeInsets.symmetric(
@ -1157,12 +1168,14 @@ class _AiChatPageState extends State<AiChatPage> with TickerProviderStateMixin {
child: messageFile(
context, message, state),
),
// متنِ همراهِ عکس هم باید نمایش داده شود
// شرط قبلی (!file.isImage()) متنِ کاربر را
// وقتی عکس پیوست بود مخفی میکرد.
if (message.text != null &&
message.text!.isNotEmpty &&
((message.audio == null ||
(message.audio != null &&
!message.audio!))) &&
(file == null || !file.isImage()))
!message.audio!))))
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0, horizontal: 16),

View File

@ -386,6 +386,10 @@ class _ExploreLatestSlider extends StatelessWidget {
data.content!.link ?? '',
description: data.content!.title,
file: fileString,
// Pass new-API UUID so news/radar details use
// getContentDetail instead of the legacy endpoint, which
// fails with "اتصال اینترنت برقرار نیست" for these ids.
contentUuid: data.content!.contentId,
);
}
},
@ -540,11 +544,21 @@ class _ExploreLatestSliderNew extends StatelessWidget {
onItemTap: (index) {
if (index < contentSlice.length) {
final item = contentSlice[index];
// Route to the correct details page based on the content's
// metadata type. 'content' as a nav type isn't handled by
// navigationHandler and produces the "اتصال اینترنت برقرار
// نیست" empty state.
final metaType = (item.metaType ?? '').toLowerCase();
final navType =
(metaType == 'threat' || metaType == 'opportunity')
? 'radar'
: 'news';
context.read<MainPageState>().navigationHandler(
'content',
item.id,
navType,
item.id.hashCode,
null,
description: item.title,
contentUuid: item.id,
);
}
},
@ -662,12 +676,22 @@ class _NewContentCard extends StatelessWidget {
margin: const EdgeInsets.symmetric(vertical: 6),
child: InkWell(
borderRadius: DesignConfig.mediumBorderRadius,
onTap: () => context.read<MainPageState>().navigationHandler(
'content',
item.id,
null,
description: item.title,
),
onTap: () {
// See _ExploreLatestSliderNew route by metaType, pass UUID so
// details uses the new-API path.
final metaType = (item.metaType ?? '').toLowerCase();
final navType =
(metaType == 'threat' || metaType == 'opportunity')
? 'radar'
: 'news';
context.read<MainPageState>().navigationHandler(
navType,
item.id.hashCode,
null,
description: item.title,
contentUuid: item.id,
);
},
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(

View File

@ -931,6 +931,7 @@ class MainPageState extends CoreProvier {
String? link, {
String? description,
String? file,
String? contentUuid,
}) {
link = link ?? '';
final context = navigatorKey.currentContext;
@ -951,6 +952,12 @@ class MainPageState extends CoreProvier {
Navigator.of(context).pushNamed(Routes.newsDetails, arguments: {
'onMarkChanged': (id, value) => markChangeHandler(type, id, value),
'id': id is int ? id : 0,
// New-API UUID: forces getContentDetail path in details state.
// Without this, details tries the legacy endpoint with the
// hashCode-derived id, gets a network/404 failure, and the page
// shows "اتصال اینترنت برقرار نیست".
if (contentUuid != null && contentUuid.isNotEmpty)
'contentUuid': contentUuid,
'args': const NewsRequestArgs(page: 0),
'hasUnmarkConfirmation': false,
'description': description,
@ -960,6 +967,8 @@ class MainPageState extends CoreProvier {
Navigator.of(context).pushNamed(Routes.radarDetails, arguments: {
'onMarkChanged': (id, value) => markChangeHandler(type, id, value),
'id': id is int ? id : 0,
if (contentUuid != null && contentUuid.isNotEmpty)
'contentUuid': contentUuid,
'args': const RadarRequestArgs(page: 0),
'hasUnmarkConfirmation': false,
'description': description,

View File

@ -85,6 +85,11 @@ class SimpleExploreCard extends StatelessWidget {
content!.link ?? '',
file: content!.file,
description: content!.title,
// Forward the new-API UUID so news/radar details opens via
// getContentDetail. Without this the SimpleExploreCard tap
// fell into the legacy `/news/<hashCode>` path and showed
// the "اتصال اینترنت برقرار نیست" empty state.
contentUuid: content!.contentId,
);
}
},

View File

@ -154,7 +154,17 @@ class SearchResultItem extends StatelessWidget {
_targetPageRouteName!,
arguments: {
'id': item.id,
// Pass the new-API UUID so news/radar details uses
// getContentDetail (new API) instead of getNewsDetails (legacy)
// the legacy endpoint doesn't know these hashCode-derived
// ids and returns 404, showing the "تلاش مجدد" retry state.
if (item.keycloakId != null && item.keycloakId!.isNotEmpty)
'contentUuid': item.keycloakId,
'args': _targetPageArgs,
// Details pages call these unconditionally pass no-ops so
// they don't crash / fall into an error state.
'onMarkChanged': (int _, bool __) {},
'hasUnmarkConfirmation': false,
},
);
}

View File

@ -107,9 +107,10 @@ class NewsDetailsState extends CoreProvier {
_currentIndex--;
}
isFetchingNewItem = false;
if (currentNews.contents.length == 1) {
getRelatedContents();
}
// Always fetch related content the previous
// `contents.length == 1` gate was a legacy quirk that hid the
// "مطالب مرتبط" section for almost every news item.
getRelatedContents();
appState = AppState.idle;
} else {
isFetchingNewItem = false;
@ -405,13 +406,16 @@ class NewsDetailsState extends CoreProvier {
_markRelatedEmpty();
return;
}
// امتیازدهی بر اساس تعداد تگهای مشترک.
// Backend already filtered by tag name don't re-filter here.
// Persian text normalization (Arabic vs Persian YEH, ZWNJ) makes
// exact string equality unreliable and would drop legitimate matches.
// Rank by shared-tag count when we can, but keep everything else.
final tagSet = tagNames.toSet();
final scored = <MapEntry<int, dynamic>>[];
for (final c in res.data!.data) {
if (c.id.hashCode == currentNews.id) continue;
final shared = c.tagNames.where(tagSet.contains).length;
if (shared > 0) scored.add(MapEntry(shared, c));
scored.add(MapEntry(shared, c));
}
scored.sort((a, b) => b.key.compareTo(a.key));
final target = news.firstWhere((e) => e?.id == currentNews.id);

View File

@ -117,9 +117,9 @@ class RadarDetailsState extends CoreProvier {
_currentIndex--;
}
isFetchingNewItem = false;
if (currentRadar.contents.length == 1) {
getRelatedContents();
}
// Always fetch related content see news_details_state for the
// same fix. contents.length gate was hiding مطالب مرتبط.
getRelatedContents();
appState = AppState.idle;
} else {
isFetchingNewItem = false;
@ -197,12 +197,15 @@ class RadarDetailsState extends CoreProvier {
_markRelatedEmpty();
return;
}
// Backend already filtered by tag name don't re-filter here.
// Persian text normalization (Arabic vs Persian YEH, ZWNJ) makes
// exact string equality unreliable and would drop legitimate matches.
final tagSet = tagNames.toSet();
final scored = <MapEntry<int, dynamic>>[];
for (final c in res.data!.data) {
if (c.id.hashCode == currentRadar.id) continue;
final shared = c.tagNames.where(tagSet.contains).length;
if (shared > 0) scored.add(MapEntry(shared, c));
scored.add(MapEntry(shared, c));
}
scored.sort((a, b) => b.key.compareTo(a.key));
final target = radars.firstWhere((e) => e?.id == currentRadar.id);

File diff suppressed because it is too large Load Diff

View File

@ -142,6 +142,7 @@ dev_dependencies:
flutter:
shaders:
- lib/shaders/liquid_glass_lens.frag
- lib/shaders/ai_orb_3d.frag
# - lib/shaders/static_liquid.frag
# - lib/shaders/water_ripple.frag
# The following line ensures that the Material Icons font is