ai voice chat edit
This commit is contained in:
parent
7ca14108b9
commit
008baa71f9
|
|
@ -1,6 +1,10 @@
|
||||||
android/app/.cxx
|
android/app/.cxx
|
||||||
android/app/.cxx/Debug
|
android/app/.cxx/Debug
|
||||||
|
|
||||||
|
# Graphify generated output
|
||||||
|
/graphify-out/
|
||||||
|
|
||||||
|
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
*.class
|
*.class
|
||||||
*.log
|
*.log
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,12 @@ package com.didvan.didvanapp
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.media.AudioManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.KeyEvent
|
||||||
import androidx.annotation.NonNull
|
import androidx.annotation.NonNull
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
|
@ -17,11 +19,37 @@ import java.io.FileInputStream
|
||||||
class MainActivity: FlutterActivity() {
|
class MainActivity: FlutterActivity() {
|
||||||
|
|
||||||
private val CHANNEL = "com.didvan.shareFile"
|
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
|
||||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(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 ->
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"copyToDownloads" -> {
|
"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 {
|
private fun copyFileToDownloads(filePath: String, fileName: String): Boolean {
|
||||||
return try {
|
return try {
|
||||||
val context: Context = applicationContext
|
val context: Context = applicationContext
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google() // حتماً خط اول باشد
|
google() // حتماً خط اول باشد
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -1109,7 +1109,18 @@ class _AiChatPageState extends State<AiChatPage> with TickerProviderStateMixin {
|
||||||
)
|
)
|
||||||
: Column(
|
: Column(
|
||||||
children: [
|
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())
|
(file.isAudio())
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
|
|
@ -1157,12 +1168,14 @@ class _AiChatPageState extends State<AiChatPage> with TickerProviderStateMixin {
|
||||||
child: messageFile(
|
child: messageFile(
|
||||||
context, message, state),
|
context, message, state),
|
||||||
),
|
),
|
||||||
|
// متنِ همراهِ عکس هم باید نمایش داده شود —
|
||||||
|
// شرط قبلی (!file.isImage()) متنِ کاربر را
|
||||||
|
// وقتی عکس پیوست بود مخفی میکرد.
|
||||||
if (message.text != null &&
|
if (message.text != null &&
|
||||||
message.text!.isNotEmpty &&
|
message.text!.isNotEmpty &&
|
||||||
((message.audio == null ||
|
((message.audio == null ||
|
||||||
(message.audio != null &&
|
(message.audio != null &&
|
||||||
!message.audio!))) &&
|
!message.audio!))))
|
||||||
(file == null || !file.isImage()))
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 8.0, horizontal: 16),
|
vertical: 8.0, horizontal: 16),
|
||||||
|
|
|
||||||
|
|
@ -386,6 +386,10 @@ class _ExploreLatestSlider extends StatelessWidget {
|
||||||
data.content!.link ?? '',
|
data.content!.link ?? '',
|
||||||
description: data.content!.title,
|
description: data.content!.title,
|
||||||
file: fileString,
|
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) {
|
onItemTap: (index) {
|
||||||
if (index < contentSlice.length) {
|
if (index < contentSlice.length) {
|
||||||
final item = contentSlice[index];
|
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(
|
context.read<MainPageState>().navigationHandler(
|
||||||
'content',
|
navType,
|
||||||
item.id,
|
item.id.hashCode,
|
||||||
null,
|
null,
|
||||||
description: item.title,
|
description: item.title,
|
||||||
|
contentUuid: item.id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -662,12 +676,22 @@ class _NewContentCard extends StatelessWidget {
|
||||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: DesignConfig.mediumBorderRadius,
|
borderRadius: DesignConfig.mediumBorderRadius,
|
||||||
onTap: () => context.read<MainPageState>().navigationHandler(
|
onTap: () {
|
||||||
'content',
|
// See _ExploreLatestSliderNew — route by metaType, pass UUID so
|
||||||
item.id,
|
// 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,
|
null,
|
||||||
description: item.title,
|
description: item.title,
|
||||||
),
|
contentUuid: item.id,
|
||||||
|
);
|
||||||
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
|
||||||
|
|
@ -931,6 +931,7 @@ class MainPageState extends CoreProvier {
|
||||||
String? link, {
|
String? link, {
|
||||||
String? description,
|
String? description,
|
||||||
String? file,
|
String? file,
|
||||||
|
String? contentUuid,
|
||||||
}) {
|
}) {
|
||||||
link = link ?? '';
|
link = link ?? '';
|
||||||
final context = navigatorKey.currentContext;
|
final context = navigatorKey.currentContext;
|
||||||
|
|
@ -951,6 +952,12 @@ class MainPageState extends CoreProvier {
|
||||||
Navigator.of(context).pushNamed(Routes.newsDetails, arguments: {
|
Navigator.of(context).pushNamed(Routes.newsDetails, arguments: {
|
||||||
'onMarkChanged': (id, value) => markChangeHandler(type, id, value),
|
'onMarkChanged': (id, value) => markChangeHandler(type, id, value),
|
||||||
'id': id is int ? id : 0,
|
'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),
|
'args': const NewsRequestArgs(page: 0),
|
||||||
'hasUnmarkConfirmation': false,
|
'hasUnmarkConfirmation': false,
|
||||||
'description': description,
|
'description': description,
|
||||||
|
|
@ -960,6 +967,8 @@ class MainPageState extends CoreProvier {
|
||||||
Navigator.of(context).pushNamed(Routes.radarDetails, arguments: {
|
Navigator.of(context).pushNamed(Routes.radarDetails, arguments: {
|
||||||
'onMarkChanged': (id, value) => markChangeHandler(type, id, value),
|
'onMarkChanged': (id, value) => markChangeHandler(type, id, value),
|
||||||
'id': id is int ? id : 0,
|
'id': id is int ? id : 0,
|
||||||
|
if (contentUuid != null && contentUuid.isNotEmpty)
|
||||||
|
'contentUuid': contentUuid,
|
||||||
'args': const RadarRequestArgs(page: 0),
|
'args': const RadarRequestArgs(page: 0),
|
||||||
'hasUnmarkConfirmation': false,
|
'hasUnmarkConfirmation': false,
|
||||||
'description': description,
|
'description': description,
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,11 @@ class SimpleExploreCard extends StatelessWidget {
|
||||||
content!.link ?? '',
|
content!.link ?? '',
|
||||||
file: content!.file,
|
file: content!.file,
|
||||||
description: content!.title,
|
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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,17 @@ class SearchResultItem extends StatelessWidget {
|
||||||
_targetPageRouteName!,
|
_targetPageRouteName!,
|
||||||
arguments: {
|
arguments: {
|
||||||
'id': item.id,
|
'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,
|
'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,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,9 +107,10 @@ class NewsDetailsState extends CoreProvier {
|
||||||
_currentIndex--;
|
_currentIndex--;
|
||||||
}
|
}
|
||||||
isFetchingNewItem = false;
|
isFetchingNewItem = false;
|
||||||
if (currentNews.contents.length == 1) {
|
// Always fetch related content — the previous
|
||||||
|
// `contents.length == 1` gate was a legacy quirk that hid the
|
||||||
|
// "مطالب مرتبط" section for almost every news item.
|
||||||
getRelatedContents();
|
getRelatedContents();
|
||||||
}
|
|
||||||
appState = AppState.idle;
|
appState = AppState.idle;
|
||||||
} else {
|
} else {
|
||||||
isFetchingNewItem = false;
|
isFetchingNewItem = false;
|
||||||
|
|
@ -405,13 +406,16 @@ class NewsDetailsState extends CoreProvier {
|
||||||
_markRelatedEmpty();
|
_markRelatedEmpty();
|
||||||
return;
|
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 tagSet = tagNames.toSet();
|
||||||
final scored = <MapEntry<int, dynamic>>[];
|
final scored = <MapEntry<int, dynamic>>[];
|
||||||
for (final c in res.data!.data) {
|
for (final c in res.data!.data) {
|
||||||
if (c.id.hashCode == currentNews.id) continue;
|
if (c.id.hashCode == currentNews.id) continue;
|
||||||
final shared = c.tagNames.where(tagSet.contains).length;
|
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));
|
scored.sort((a, b) => b.key.compareTo(a.key));
|
||||||
final target = news.firstWhere((e) => e?.id == currentNews.id);
|
final target = news.firstWhere((e) => e?.id == currentNews.id);
|
||||||
|
|
|
||||||
|
|
@ -117,9 +117,9 @@ class RadarDetailsState extends CoreProvier {
|
||||||
_currentIndex--;
|
_currentIndex--;
|
||||||
}
|
}
|
||||||
isFetchingNewItem = false;
|
isFetchingNewItem = false;
|
||||||
if (currentRadar.contents.length == 1) {
|
// Always fetch related content — see news_details_state for the
|
||||||
|
// same fix. contents.length gate was hiding مطالب مرتبط.
|
||||||
getRelatedContents();
|
getRelatedContents();
|
||||||
}
|
|
||||||
appState = AppState.idle;
|
appState = AppState.idle;
|
||||||
} else {
|
} else {
|
||||||
isFetchingNewItem = false;
|
isFetchingNewItem = false;
|
||||||
|
|
@ -197,12 +197,15 @@ class RadarDetailsState extends CoreProvier {
|
||||||
_markRelatedEmpty();
|
_markRelatedEmpty();
|
||||||
return;
|
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 tagSet = tagNames.toSet();
|
||||||
final scored = <MapEntry<int, dynamic>>[];
|
final scored = <MapEntry<int, dynamic>>[];
|
||||||
for (final c in res.data!.data) {
|
for (final c in res.data!.data) {
|
||||||
if (c.id.hashCode == currentRadar.id) continue;
|
if (c.id.hashCode == currentRadar.id) continue;
|
||||||
final shared = c.tagNames.where(tagSet.contains).length;
|
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));
|
scored.sort((a, b) => b.key.compareTo(a.key));
|
||||||
final target = radars.firstWhere((e) => e?.id == currentRadar.id);
|
final target = radars.firstWhere((e) => e?.id == currentRadar.id);
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -142,6 +142,7 @@ dev_dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
shaders:
|
shaders:
|
||||||
- lib/shaders/liquid_glass_lens.frag
|
- lib/shaders/liquid_glass_lens.frag
|
||||||
|
- lib/shaders/ai_orb_3d.frag
|
||||||
# - lib/shaders/static_liquid.frag
|
# - lib/shaders/static_liquid.frag
|
||||||
# - lib/shaders/water_ripple.frag
|
# - lib/shaders/water_ripple.frag
|
||||||
# The following line ensures that the Material Icons font is
|
# The following line ensures that the Material Icons font is
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue