commit ef509a6623c5cdcc93e5dc5dd2135cdd1485fdca Author: Mr.Jebelli Date: Mon Feb 2 15:58:27 2026 +0330 first changes for bassa diff --git a/.firebase/hosting.YnVpbGRcd2Vi.cache b/.firebase/hosting.YnVpbGRcd2Vi.cache new file mode 100644 index 0000000..e849190 --- /dev/null +++ b/.firebase/hosting.YnVpbGRcd2Vi.cache @@ -0,0 +1,267 @@ +manifest.json,1746355382339,a521bf8f8473f9ee1898fa47017e3ba828743e17fb6a07810037282eed2a0455 +version.json,1748779821992,4809f51eb1be62b025569a76187fe404bb7df6dd44c0fbf3fa386cf2315f1b0c +home.html,1748501953626,6c7ce01f48a875e9820a742dd825eed5cad49ba72e2aaf6ad6b0541719ba76ac +index.html,1748779762226,6cef43db3e3893e1d7e240eed7e1cfd6f10cdcbb986285262757c4c0b0b73508 +flutter_bootstrap.js,1748779762156,89ca3ddf3cd3c09f2292a16e49a9b72dc002ac0cfd3085ea8c97f606aefe8a63 +flutter.js,1744650586322,843b4175654fd94708022d55eaaed1c524df379e733a4c27c8168450143016e9 +favicon.png,1746355382335,6b30044f011f4c01c4b84801c6c4dac2c1481551dece03fb165126a372412e15 +flutter_service_worker.js,1748779831690,ac427b5d6689b3a0ef7903cefa14e3c87973be5d7e771c7546dd54a1f9017eff +canvaskit/skwasm_st.js,1744650586301,7f097811fb848295340c14deb0cf491d63923436f77c4fc9fd06f66f62c5b901 +assets/FontManifest.json,1748779822721,07e90e73002cc681dc0d8a9cf11f557fa6218a693bd97e0ec1202cc731d6bbc8 +assets/AssetManifest.json,1748779822720,7194c6be169f944a70102acb3a6c1b99dbd484d2baa4e466166bc38ae55e6673 +assets/AssetManifest.bin.json,1748779822720,4bc02ed82a870db76e84090a94452c70eca78c2d80ffb4bf4c59b063c13ef215 +assets/AssetManifest.bin,1748779822720,f0c09fa650cb47d8ae9ac89b854a75ff187d83382f65e88d4d9a12bf66ba661a +assets/shaders/ink_sparkle.frag,1748779822943,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406 +assets/packages/flutter_sound_web/src/flutter_sound_stream_processor.js,1746356349820,a739bf9ddb3e7abd9cba2baae435c73d4dc6b66aa18e763d1a180f2e74df44df +assets/packages/wakelock_plus/assets/no_sleep.js,1746356275458,408f7129199975b0730901df94d3d5a1b79e75600bc33ba6e042d316d771876f +assets/packages/flutter_sound_web/src/flutter_sound.js,1746356349820,f9474ac650ce6e6e566a7dd6c374cd627e31b234ae03c1e42b99b788dbae76c4 +assets/packages/flutter_sound/assets/js/tau_web.js,1746356687614,3f09690e80abcc9494056e21205be71c924ad774b53d2a234d2e0f584eb4b0cf +assets/packages/flutter_sound_web/src/flutter_sound_recorder.js,1746356349823,bb2efe7a86e3a14d30848bcb33c05d816cb125b9d50bc1881b4c5a02e61b9208 +assets/packages/flutter_sound/assets/js/async_processor.js,1746356687613,a023aa0428017299d14a8357eeaea0473e045069bf063dbe90ad43f539c4cc9d +assets/packages/flutter_sound_web/src/flutter_sound_player.js,1746356349821,00ef444ba18d2a8aec15a74142d2252cca4a4c74e46910967f1e6f4afc036406 +assets/packages/flutter_math_fork/lib/katex_fonts/fonts/KaTeX_Size3-Regular.ttf,1746355927006,cc047859914502c7224d8a2b14efa92e3da86b6b165aad8b552fc6a318f5dd7e +assets/packages/flutter_math_fork/lib/katex_fonts/fonts/KaTeX_Size4-Regular.ttf,1746355926970,b538e24ce9d18db660f5104515e4bc96120214fbc29ae478e3ba64508b07b756 +assets/packages/flutter_math_fork/lib/katex_fonts/fonts/KaTeX_Size1-Regular.ttf,1746355926978,7baaa6ea42631608b5485a3b168b5e56bcf391e1bf2d676807784fc7e0882372 +assets/packages/flutter_math_fork/lib/katex_fonts/fonts/KaTeX_Size2-Regular.ttf,1746355926982,670b56650a1d8fbe2f03995b082b64b11df8aad85a54f1649904d494a6a670ea +assets/packages/flutter_math_fork/lib/katex_fonts/fonts/KaTeX_Script-Regular.ttf,1746355927007,0a77a5f7799171f36257c6b7389d11c5b33859c992901a5a54802f97d6fa5a21 +assets/packages/flutter_math_fork/lib/katex_fonts/fonts/KaTeX_SansSerif-Regular.ttf,1746355926980,bfeaaa8d52c3f8067e96a003a078af91798c8b4f9a3e13c82e8c5965f31c27f0 +icons/Icon-maskable-192.png,1746355382336,74f8888f69a5deb403e84aa44f6b6a713c2a048752d10ebb4b6018fcdc50992a +icons/Icon-192.png,1746355382335,74f8888f69a5deb403e84aa44f6b6a713c2a048752d10ebb4b6018fcdc50992a +assets/packages/flutter_math_fork/lib/katex_fonts/fonts/KaTeX_Typewriter-Regular.ttf,1746355927004,a51fa150903eafc15c14ab525e9d5d738fc1f95002f7c2b45727cb9f1c12cd2f +assets/packages/flutter_math_fork/lib/katex_fonts/fonts/KaTeX_SansSerif-Italic.ttf,1746355926991,818ae9527c18c5d8424721425769240deb55de70db550c3544f4fc9a4c4495a4 +assets/packages/flutter_math_fork/lib/katex_fonts/fonts/KaTeX_SansSerif-Bold.ttf,1746355927001,201abae1d27b5ecac5b4a0e31772c4c5823ed2292fe0c61c71716cd64f23abbe +canvaskit/skwasm.js,1744650586277,e8556ec6ef77a92c1c650103ebf9a7df3642bf24f5ba95820df96994586b482c +canvaskit/canvaskit.js,1744650586213,5fe5c53e4b2490d8a22b03d981d397d548049ea58077d827203d6137337066fd +canvaskit/chromium/canvaskit.js,1744650586245,9186b0125658af75ac5dedc9b1a787cf754d23d1f3d61bf2e144898bd6aa2ba2 +icons/Icon-maskable-512.png,1746355382337,f29eb710be2326cd839930c89193cf49051228388b3073de093b4ec545aa0c22 +icons/Icon-512.png,1746355382336,f29eb710be2326cd839930c89193cf49051228388b3073de093b4ec545aa0c22 +icons/houshan-icon-whie.png,1746355382338,aea9d2f9426822f202a53aaf4407c2291740d44eba6bf06595796fafef3b793e +assets/packages/flutter_sound_web/howler/howler.js,1746356349839,037b4958cd39fabfe1ab9de926f007c9543b708a1adae8dcee4f2f9846f12efa +assets/packages/flutter_math_fork/lib/katex_fonts/fonts/KaTeX_Fraktur-Regular.ttf,1746355926994,a79b88979c5ac8e7df60fcd988baa10568c3b143254e02c4fdfb3b39adcffa26 +assets/packages/flutter_math_fork/lib/katex_fonts/fonts/KaTeX_Fraktur-Bold.ttf,1746355926984,cbc04994cefcba152ef44e923d2b652f338a5bea81a95fc0b0d47ffaed0361d8 +assets/packages/flutter_math_fork/lib/katex_fonts/fonts/KaTeX_Caligraphic-Regular.ttf,1746355926976,b9b27fa6f49e79337e2d020b8a4a01dcf67496ea6c665454bd756d4df0d86111 +assets/packages/flutter_math_fork/lib/katex_fonts/fonts/KaTeX_Caligraphic-Bold.ttf,1746355927002,ce2029756a25d8d8cfdce4d06f2a39861599831cc8e7d371e6f1cd5c2d62cfdf +assets/packages/flutter_inappwebview_web/assets/web/web_support.js,1747657092008,76406ad60c510590d72b4abc64542600eea9ce0e0dff2bcf012f21d101f7e106 +assets/packages/flutter_inappwebview/assets/t_rex_runner/t-rex.css,1747657099878,4c189f736f38191c36958dfebcecb978413706129dff77cff3fe99940cc621e2 +assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1748779826856,72702838d980b01ee05e8ce5519c5d038f73c501e7625982d08f560f8cdfb78a +assets/fonts/MaterialIcons-Regular.otf,1748779826863,2c9b47ea478b493eb7ce74db87d32ccf4c387797f7577ea4e28f51ce9855d2ba +assets/assets/image/app-icon.svg,1748498662145,acdd18aee619c501e96af042e291d5557f6a1e72e71d2526154b9d69552d5526 +assets/assets/image/app-icon-primary.svg,1748498662144,0c794ee7ccefe5faef3d8ee2079bd65fd0018b823bde62bc9d3488cd869548e0 +assets/packages/flutter_math_fork/lib/katex_fonts/fonts/KaTeX_Math-Italic.ttf,1746355926987,aa1ddf351d86421a7b00aadc77b5b13e9aa467336dd9b9ff05b1e6dbe3a1cc5a +assets/packages/flutter_math_fork/lib/katex_fonts/fonts/KaTeX_Math-BoldItalic.ttf,1746355926966,b05cb2f1d844514bbc3ab742762a23310220fc7bd77232e25f87fef4a8294794 +assets/assets/image/empty/reverse-arrow.png,1745395920000,4758b2fff720e38847332e17f586d032d1821f7cdfd030ece4f460b3b26a5938 +assets/packages/flutter_math_fork/lib/katex_fonts/fonts/KaTeX_Main-BoldItalic.ttf,1746355926968,b8e7187caa624c371f3e4a72c6f63f795a5ee0ddd90db3a6d5609568404dc8b4 +assets/packages/flutter_math_fork/lib/katex_fonts/fonts/KaTeX_Main-Italic.ttf,1746355926985,375b812bb8543923304ed0331ef469d6287168bc84d65092b87bddc0c49f2eff +assets/assets/image/income-steps.png,1745395920000,89303748a0f00bb232fdc79f6b9684017e3b7e2f7447082b9fda938af740a9e2 +assets/assets/image/empty/empty-text-underline.svg,1748498662146,3e7f823d730138d402b9d7d16078c9c478b2ef1ae3d083bf8c757422427112b5 +assets/packages/flutter_math_fork/lib/katex_fonts/fonts/KaTeX_Main-Bold.ttf,1746355926999,db463dc580c027e6f68e55aac55ec8450c72d307673a4cab2b7f186b6613a584 +assets/packages/flutter_inappwebview/assets/t_rex_runner/t-rex.html,1747657099874,c02eb6fbbb6aa25b6cbc4eccc34369717449319fe552bb6af32e52603a782f26 +assets/assets/image/splash/splash.png,1745395920000,bb43e1f9679a4438febc79a417b4d5b6c40638d4149ef22e9ccb8fefa5ec4139 +assets/packages/flutter_math_fork/lib/katex_fonts/fonts/KaTeX_Main-Regular.ttf,1746355926993,1a5accbac4c7234144db3ea7b319fc6cfcd8b21b688089ebd9aa2f42a6707b83 +assets/packages/flutter_math_fork/lib/katex_fonts/fonts/KaTeX_AMS-Regular.ttf,1746355927010,dd1d2a8317292ad591ccf823965d9d1ed09905fdf3eade937f3b1e793f19b479 +assets/assets/icon/social/bold/youtube.svg,1748498662143,6c64e807eeda79a09343db60417eb3cd6025f38402992a778429841e94d27586 +assets/assets/icon/social/bold/twitter.svg,1748498662142,5637760780585205dbfe9398505c19cd26849539c27c028a8b205d688e62256b +assets/assets/image/boardings/AI%20Houshan.svg,1748504283399,71088d4be0a589a64632b966fca618bb481e6b183bbebebc604df9abbbc7e63f +assets/assets/icon/social/bold/telegram.svg,1748498662142,8f6533b5071772560d463e2aaf632cdfbe4684ece6ad4a92631798fa36e56128 +assets/assets/icon/social/bold/site.svg,1748498662141,821f12b918453c778b5871f386d190b0f852142f5e053c9f99ebdf636f0674fa +assets/assets/icon/social/bold/instagram.svg,1748498662139,74d563f25e8a440fb4e04fd3237392327affcbd43e3b023a3c511e6aa426b418 +assets/assets/icon/social/bold/linkdin.svg,1748498662140,53cde6a96346702d0abbf12780ce4e66c5b069d913aac70b42122d80b42a749f +assets/assets/icon/signin/igoogle.svg,1748498662138,a73ad43a3bad636eed4b215b2f6b77e893079840c30179df4e8295aa34bbaf3c +assets/assets/icon/outline/warning-2.svg,1748498662138,7eaa1ea754317f7c6379fffce0aca80abcec5cbf0874394ce8666f7b7c840b0e +assets/assets/icon/outline/video-play.svg,1748498662136,dc096e72dc17d6173558604fabfbd85d85528f0198d235dbcf78002e7dda13c9 +assets/assets/icon/outline/voice-cricle.svg,1748498662137,20206a6551589ec404c84f43c60a41730b6a6d65c0c9131130ebd13a28a5b8f5 +assets/assets/icon/outline/verify.svg,1748498662135,ca3fb3592e0e8f921e8ec6a030dd26bc888abdd40ea7846210e9a94df6e767fb +assets/assets/icon/outline/trash.svg,1748498662134,c25d6fcf4dffc2238a26c3c617cb192db1209d19ceccf5fa22107de52bfa311d +assets/assets/icon/outline/translate.svg,1748498662133,db645503e14122335b8e4477a6888ae45b537a7f75959e13cb0fab3333e2aebd +assets/assets/icon/outline/tool-box.svg,1748498662132,7376e059083f616bdbc9da6c160608bff430e18dcb9961c74b93dd486fd75e3d +assets/assets/image/chat-back.png,1745395920000,a2b923478317067a754b55209938c3422f4d40d496043368c95906cf300cf3ed +assets/assets/icon/outline/timer.svg,1748498662131,12853b357618a544011c8046bfa0539564abc04fe6ce6a9a6149cc92f7274b80 +assets/assets/image/splash/splash-desk.png,1745395920000,cc56af09174a5fbc58aa4ed8b5f52f94294fcb65c7330146552be5ed9272a9c3 +assets/assets/icon/outline/tick-square.svg,1748498662130,f2b6ec666a8bad74f9b69dd9d5a1dfbf1de6d37c0ae40bd53321393b2fd35ada +assets/assets/icon/outline/tick-circle.svg,1748498662130,d81daaa263f19861b01ebba3eb8f189b63c41cd55dbc764c18df62c79037fcaa +assets/assets/image/video-back.png,1745395920000,a13dc4db3ab5b70f6af125721ec9aac2a8d93c50f3f336356b7b9e8376519eaf +assets/assets/icon/outline/sun.svg,1748498662129,8c6922f554590ac2334a230db365c694f8088ab7493ec9c5723278bd7681ec9e +assets/assets/icon/outline/stop.svg,1748498662128,847381a790251dcc1dfcc374f634ab497e4acae1204df424f67db8bc9a2e2edb +assets/assets/icon/outline/stop-circle.svg,1748498662126,c3d7c4bb0166831dc2d5abd6169ccde2463893658d145f5dce185de3fd6999a3 +assets/assets/icon/outline/sms-tracking.svg,1748498662126,e7c2a7040b817c3d784f1c39fdc1a55f527b2dc1b8feedac6308700e14c70b00 +assets/assets/icon/outline/shield-tick.svg,1748498662125,a92e3c9e0697691eb76e5831f1a265ffbbe84088ecbdc61ade4e0112126f5f5d +assets/assets/icon/outline/share.svg,1748498662124,a44ab6404f565cca87400999cd24f882755a576055d1f2e13e9fc52fadddfae0 +assets/assets/icon/outline/setting.svg,1748498662123,36e9d953fcacb8e4590e20ea446b71ef98daf2f7d9e43cbf818f48bca532a1ca +assets/assets/icon/outline/search-normal.svg,1748498662123,504d9e734ef9c928da3d1957e6fba241970527beceb11a7a3718e495c6469cf8 +assets/assets/icon/outline/profile.svg,1748498662122,898674249e3fdaaabaa8af63a3deaa051107ff19b9e28aa409a9879af44c98a6 +assets/assets/icon/outline/profile-user-doual.svg,1748498662121,2365ae95e1f427aa4cc6be02811b29c4778c13156a0db9124228b044f0ea0134 +assets/assets/icon/outline/play.svg,1748498662119,336ed6b428050e655105251327cf00ad5c81edeb68b9b9ca25324d2a0b170b04 +assets/assets/icon/outline/profile-tick.svg,1748498662120,400a03153ac95b13a4193c1031234547a3b66ef90bc0b95385698f18ec412f9b +assets/assets/icon/outline/pause.svg,1748498662118,d6d7594da57673e0d69d43b7151962c3cf5fd44435d4475de09569b29bd63370 +assets/assets/icon/outline/notification-bing.svg,1748498662117,b8c8f27ce972e04c9d2517f25f87650f13309cffb28eb4b66f3005b75a352a5a +assets/assets/icon/outline/news.svg,1748498662117,52c074fd13a7d3da12b468f124f439255a87fb94a5ad2fdc363aa576a5452222 +assets/assets/icon/outline/more.svg,1748498662115,e17ad5c5aa41317c3f589e68a13400cfde1cdaa57ba0405dae3642b13447f65a +assets/assets/icon/outline/musicnote.svg,1748498662116,5c13654bd671bc5bcdd2dd95db7fecf75e3284dfc3f722160dd65720c1d1d0e2 +assets/assets/icon/outline/moon.svg,1748498662114,21c55a33423929b74d60f9c7340ed3d4c4779d3dcc0176a2f710eccb588746db +assets/assets/icon/outline/mobile.svg,1748498662113,0a38d70afce09ecb02532676cfa96c54cad6635e2e5633db6ad9fb23b0daad9e +assets/assets/icon/outline/microphone-chat.svg,1748498662113,3c3477e1d5b5f76a71774cf00032bd7de1558a62ef154685d5edc3fa06aa9920 +assets/assets/icon/outline/messages.svg,1748498662111,440dc8e0cfd8a095b53be18af989905692f743ac586682aaa06f7a01fb70640e +assets/assets/icon/outline/message-text.svg,1748498662110,93d2618bf47f67f66c75c058aec978ab9b09e624daee4394ee4024752e3ec01a +assets/assets/icon/outline/message-question.svg,1748498662109,992823a7430fff3b504989f1fbe01474390ebe7c70ae4568a1928c4545d17110 +assets/assets/icon/outline/media.svg,1748498662109,b5e2576778a753753b23263e8d040531a430baec6332823010428965d933c843 +assets/assets/icon/outline/magicpen.svg,1748498662108,32bc9983d2385c0f92be332fcdb50ade8cc598174e529c04975b87ec38924ce6 +assets/assets/icon/outline/mage_scan-user.svg,1748498662107,22ff26a913eb7cb14f618c42c9fccd18a9ff5d70d51ed7b494b3cf5797edad8a +assets/assets/image/empty/server.png,1745395920000,c2d9ca9dc8602258f812248fe72813d4d2a9c1b103c71cf17811740843a6e397 +assets/assets/icon/outline/login.svg,1748498662106,4b66008c0249b520f8336d83dc32a44d053cb04d5f96040d3573f4097c636232 +assets/assets/image/empty/messages.png,1745395920000,cb818d45777fc7251ccc1732b7bfe8652984665afc6b94472662ac9963a237b1 +assets/assets/icon/outline/lock.svg,1748498662105,41296be8a727caef1325f9737de2a1ad997bdb4a07e199f5641a5c811a8b0297 +assets/assets/icon/outline/like.svg,1748498662104,1a1304187276ab159c3085a3c2cef57b93a4b28d6a30d275b8cbaeb48f3be076 +assets/assets/icon/outline/library.svg,1748498662103,e27d8515b37d4aea12234ecfe14c4f64b960e17b24ed4efed62ffe7796c3f28b +assets/assets/icon/outline/lamp-charge.svg,1748498662102,9a31e65895d4ffb0abda2478fa0a2a20ef3a10bd1f6e78999a77c206b76536e3 +assets/assets/image/empty/inbox.png,1745395920000,8e618e28bec5c054abfb1a3ef02b4a995aa85b4d04a15d10f0b7a5b6273d7f39 +assets/assets/icon/outline/info-circle.svg,1748498662102,103869a2e48f8c4f271595d87d44e080ccd7adfea4cbd44118597f433b37d114 +assets/assets/icon/outline/import.svg,1748498662101,2c401f610b58b0cba2bf04a743eae291d6506de30b4f7f9f95eb722107c8d68d +assets/assets/icon/outline/idea.svg,1748498662100,90f61ab73e7e72b0723efb88ad163778481eb6ec1569b5ddf0e884c2cc076c5b +assets/assets/icon/outline/home.svg,1748498662099,a7d41168ea09fe17830c7cde000b14f1dcb1eb58d7aafc3d4e437592a0ecf9db +assets/assets/image/empty/connection.png,1745395920000,9aa094b4a7cf50b990b33ef61c47583ed34e38b4115157eead87736e9852b649 +assets/assets/icon/outline/heart.svg,1748498662098,d700c787f353e380db22d228cbe053db483b6661fc8cfa4f2efb0e4f92e01bcf +assets/assets/image/empty/assistant.png,1745395920000,e5dcdfad6b6982b5b824e17533ae951ad1deb896b5171fada36ff374341b02f6 +assets/assets/image/empty/amount.png,1745395920000,2d712e05507ecb1fcc59f0d481dbff0a60f31eef75ff01d89b4ba0d27520e7d5 +assets/assets/icon/outline/hat.svg,1748498662097,5fa04932ae9f138e86038880c08d071eba5005d875cd9af13580a3cd94a1d186 +assets/assets/icon/outline/global-search.svg,1748498662096,20630ea4ac864b5bf5e0b6b0da0c07c44cb4fb6588843f61235e9e9686b9a4ad +assets/assets/icon/outline/gift.svg,1748498662096,edf766582ad259416955d554eedc0c0241027affe451398e0d408b082aac86fb +assets/assets/icon/outline/ghost.svg,1748498662095,df50365b34913a231dee74ba9845fdf16b11b00ffb71293bbf97f47f605b13a4 +assets/assets/icon/outline/gallery-add.svg,1748498662094,5d21fc408ba39a15e61a40c53870b37cbb46b744366b2b98fac938596d6713b1 +assets/assets/icon/outline/flag-2.svg,1748498662093,454da97a5b7742e8d3eefe0bc36b3c124761492c5114183f695f08b0739ee621 +assets/assets/icon/outline/filter.svg,1748498662092,d25bd44ba47ddbe24fd2e20e006c82cc7b53875ad4241edd1f26be5297fba668 +assets/assets/icon/outline/eraser.svg,1748498662091,dad77ea81068a2d83dda564adfdfd5ca0e85d00355bbb065083e8aafe6c4664b +assets/assets/icon/outline/empty-wallet.svg,1748498662090,27d04c58db24929eb72a243439249e007ef8b2a10db5ff2d814c8d3f08aa1018 +assets/assets/icon/outline/empty-wallet-tick.svg,1748498662090,6099d493b15e454ce14a13a2adad4e907433e1bb850db46025bec0bb8d32348c +assets/assets/image/audio-back.png,1745395920000,36279f308289b10de049007d2debc1d6f9bbe5355b56848b8234d30845fbfcbf +assets/assets/icon/outline/emoji-happy.svg,1748498662089,ffec248ef85fab60a0e6bb242e9e1a253266dac6f4c79f7dc195bcdd5034e78c +assets/assets/icon/outline/element-plus.svg,1748498662088,31d7ebbc82fc268c5e3e1a5ed53d54479153fa13631295c9d2ab06946b09f43e +assets/assets/icon/outline/edit-2.svg,1748498662087,4df60b1ca5d536610d18d0a92718a85361bdb9153ed8c78f69c5bbbfaf32960a +assets/assets/icon/outline/download.svg,1748498662086,b4e8c7e49ce89b9b939fde69bcf9a62d7b42bb2df9c3bc7403c045434aee1ce1 +assets/assets/icon/outline/document-text.svg,1748498662085,dbfadc3cc01e1489bda6b4ac8270edaf9d630be1c0614aab75be377535081300 +assets/assets/icon/outline/document-copy.svg,1748498662084,b0691b0d5fad627a76f3960a08e8024f30580561ae7efa6e4574a809cd32bcca +assets/assets/icon/outline/dislike.svg,1748498662083,2224859ed9c6fa8c52c0156d7290ffec8d151ecbf293e1ebdd22253a5bc0cd3a +assets/assets/icon/outline/direct-send.svg,1748498662083,ffcebbb7ef072d8c4c1b27f5c4fd5d3c8dfa3c2cb74afa2a9eda81c5979e8195 +assets/assets/icon/outline/direct-inbox.svg,1748498662082,a163bbfc171af0a89beb7696a88db42438d0302a5585fc4a7780f2e17eef5e0a +assets/assets/icon/outline/crown.svg,1748498662081,83ed20f39a047470d27fe74d4bd584c95dd50f71e09bf28d92f9552e25310b98 +assets/assets/icon/outline/courthouse.svg,1748498662080,80163df325ffa410b33e695059afa05457a8f1f3796298a5dc0e8661042c7d0b +assets/assets/icon/outline/copy.svg,1748498662079,b146ee8917e7ebe29a5aa76c4d92e238d462638c3eb7b0aeaffe783b027b53e6 +assets/assets/icon/outline/coin.svg,1748498662078,c6ca38c183574b65469d966d27ab5412ecacd069ad703daf3e96791e7a79101e +assets/assets/icon/outline/clock.svg,1748498662077,7d1c4cfb222b471b08bd63fb32dc86f2b275c7a18381efe6eb9ec52cde1ea369 +assets/assets/icon/outline/chat.svg,1748498662076,b1d78233b883ad6406a95d4a1380d569962d4b68d2edcefedbb0b72cb27ccf9a +assets/assets/icon/outline/characters.svg,1748498662075,e57c8e3a76f7468ca64c88a25e1fcc773766b2e8ffde5096246a2bfe993cb2b3 +assets/assets/icon/outline/chart.svg,1748498662075,ae5552e4dc5ac74032e439fc48f428af853cc7671e2b1a3d1e16a014646f8440 +assets/assets/icon/outline/card-pos.svg,1748498662074,beeef8cb4ec1fa58959dcb7033c50e41a5aae951712d765f72dc26c2ccd64cb4 +assets/assets/icon/outline/card-add.svg,1748498662073,aefe3db8e7cc0ddb491aa537e00a29948b9ddb54f983d9399e86b3123851e9f6 +assets/assets/icon/outline/camera.svg,1748498662072,c3799a59011072340eb5e3b5cf6107ec9aea4de17e6b81247e4d988dee99ac09 +assets/assets/icon/outline/call.svg,1748498662071,a2fc6b19639d8fc0a991bac00cf6e7ae639f2be884ee8808d74a68b0b79a83d9 +assets/assets/icon/outline/calendar-edit.svg,1748498662070,e60246a7e2167d0987568f5cb644097047fcccd6b7381a9fd32faeb32ef008aa +assets/assets/icon/outline/brush.svg,1748498662070,99a2d804402696e6b2c38a28c7bebc766a1f6cb6a7b8d005c5dfee359a620c2c +assets/assets/icon/outline/brain.svg,1748498662069,f673e47fb205864a0162b8caaf04a61b507b4ac7674990396bfb706f880ceef7 +assets/assets/icon/outline/bitcoin-refresh.svg,1748498662068,5d5869c522a057dce4b1b60f2e2e81c8ca55582dc5870fed7058dc2a8cdab13f +assets/assets/icon/outline/assistant.svg,1748498662067,a84b1b10137d49edc2b3e48a86919a890577f365731d3ce17a166e4fc28e5855 +assets/assets/icon/outline/arrow-right.svg,1748498662067,6cd7612284d85d34ffdecc7d1e01d52b8f5d3c787dbd81ef4846f5ca4d52c3b6 +assets/assets/icon/outline/arrow-flash-right.svg,1748498662066,0d8b69e73622a0bf7bcc94a7cf6fc3bffdbf5f8e7e27d31f2f12060c749997b2 +assets/assets/icon/outline/archive-tick.svg,1748498662065,52e41364f5fcca40bfdbfe6207a413dad2780a8f1b50fcde1ee010349a4bc3de +assets/assets/icon/outline/add.svg,1748498662065,779d3322f21b7cbcc70811741c1c860be79b92262f4d8a753174f034b76e61f0 +assets/assets/icon/navbars/navigation-light/setting.svg,1748498662063,a7c1b26b897f5fb6573c7687163e62aaf332dc3f571e8e2b395d9776aea6c476 +assets/assets/icon/navbars/navigation-light/media.svg,1748498662063,a86310485f685597deff7c8a0a039b5f8e7c608af9530b155e293b140ff255c8 +assets/assets/icon/navbars/navigation-light/home.svg,1748498662062,ebfc59cdd9c3eff3d9584f67651e2b98e58235afef2a8af2c0f99751c5df5ace +assets/assets/icon/navbars/navigation-light/characters.svg,1748498662061,c9cd537fbdb7303bc8e2695c2b228b5a8e3f8c301d69096937d0635fb3051aef +assets/assets/icon/navbars/navigation-light/assistant.svg,1748498662060,321217f57d830bfc7ab15e439a1036e13bb57b4eed122f865f5e0b56c7b92fef +assets/assets/icon/navbars/navigation-dark/setting.svg,1748498662059,c044cdc5f51dbef1a0ff0b6fa1d93fd44ee948ed0a865e326948c1c4bd5eaca0 +assets/assets/icon/navbars/navigation-dark/media.svg,1748498662058,fa88f334f63db675bffd77b11faf52bf983eaf8361c8748939067a54e7faebc2 +assets/assets/icon/navbars/navigation-dark/home.svg,1748498662058,dace53fa4b460cd8c69aac829e86ee4f763973a750e6d84c2f18e0737ef021f7 +assets/assets/icon/navbars/navigation-dark/characters.svg,1748498649563,74eb2c50905161cdc6bf953b219328cd1fa026eb10e5b8bd0be0c1365f3ac018 +assets/assets/icon/navbars/navigation-dark/assistant.svg,1748498649561,a6a6bae6ec5516312f66bccaf9bfdf735d5091dddda0e54647e7de58bfecfe74 +assets/assets/icon/navbars/navigation/setting.svg,1748498649570,36e9d953fcacb8e4590e20ea446b71ef98daf2f7d9e43cbf818f48bca532a1ca +assets/assets/icon/navbars/navigation/media.svg,1748498649569,4b450340e347a30b159559d52997867375b906eff42e24fcfc4f0cdf4c24c430 +assets/assets/icon/navbars/navigation/home.svg,1748498649567,cbb5b43ce2fe59e5f99121ecf70df803e305f0bcde0b103de88d554fe6674056 +assets/assets/icon/navbars/navigation/characters.svg,1748498649566,b74a288191f77a83610a5238c923b404135c8ba88335dbc5d73522c8d5cd73c0 +assets/assets/icon/navbars/navigation/assistant.svg,1748498649564,ee9424e5c04db2b464cdf90c3df3f0185c297ab086e0e28d347606290b00c496 +assets/assets/icon/launcher_icons/houshan-icon-rounded.ico,1745395920000,896ebc8fd2dad1ccee8523d2c433ab7992f03f1500a4c365f8b0e5d3d372e22c +assets/assets/icon/gif/flash.gif,1745395920000,f8fcd4cce866bcf16bb1517135a598c3493b511aab0e53b580b39e7c5e0e7b4d +assets/assets/icon/gif/empty-bookmarks.gif,1745395920000,61108e55c5800193d06dbb686603fdf18ee0daf9381f81a532a6a6f58cb969b9 +assets/assets/image/expected_format.png,1745395920000,9ee4955b89ee07de6b4c9cedd21552f5b51bae759856b7d98a03260adb61d660 +assets/assets/icon/launcher_icons/houshan-icon-whie.png,1745395920000,aea9d2f9426822f202a53aaf4407c2291740d44eba6bf06595796fafef3b793e +assets/assets/icon/bulk/warning-2.svg,1748498649560,b306c567c4c9446e7423e71bcbfee34eb4eb48ddf58146c4eb7dd65abefc8a27 +assets/assets/icon/bulk/video.svg,1748498649559,a645c8398e9e0f45a0a22aee31f47523a4e79214c2b60683839eb79581792c48 +assets/assets/icon/bulk/tool-box.svg,1748498649558,d0396d2060b0743bc30fc6f1048b0d992be8c06ce1a4792a5fb02a26e4f6446f +assets/assets/icon/bulk/setting.svg,1748498649557,a7c1b26b897f5fb6573c7687163e62aaf332dc3f571e8e2b395d9776aea6c476 +assets/assets/icon/bulk/setting-dark.svg,1748498649556,c044cdc5f51dbef1a0ff0b6fa1d93fd44ee948ed0a865e326948c1c4bd5eaca0 +assets/assets/icon/bulk/news.svg,1748498649555,8da5a418e1843a2ef407bc542636a04a5e86ec52dc232744b244a463006b7cfb +assets/assets/icon/launcher_icons/houshan-icon-primary.png,1745395920000,3da553ed2bfcc4a5a519221af540b19baf8ba124758aa5fb9f0ef2e24cbcb83f +assets/assets/icon/launcher_icons/houshan-icon.png,1745395920000,214b6e1d1779e287085543a3767e97efbc937958c935e550ea1bcdb89808e151 +assets/assets/icon/bulk/messages.svg,1748498649554,bc708b43197ab9fdf20f8a44abcb8514ecd1ea73e2d645a888d3eb887a13c4d8 +assets/assets/icon/bulk/messages-dark.svg,1748498649553,78fef4b88bf1a8fdc93ecb7b8cf0c574c6fee53e52588f260d34e0e5fe595155 +assets/assets/icon/launcher_icons/houshan-icon-midround.png,1745395920000,9fbc4f4da0f92fc64c4b64119fd5bfeb1632995de33d284bd176e1db9daf6903 +assets/assets/icon/bulk/media.svg,1748498649551,d346f514c5fa5adb404a641717073c6adf40ce58388197c2a139b82deeac3f87 +assets/assets/icon/bulk/media-dark.svg,1748498649550,fa88f334f63db675bffd77b11faf52bf983eaf8361c8748939067a54e7faebc2 +assets/assets/icon/bulk/library.svg,1748498649549,7ceb471a2d5fec3217d8b19c91e83d9ea875c1022a0fb242fa1b95e02cbd6fb9 +assets/assets/icon/bulk/library-dark.svg,1748498649548,19fce1df029d24e952a1742a61829412ffe6a8bffd2eac8a5e035b4349b961ed +assets/assets/icon/bulk/home.svg,1748498649547,ebfc59cdd9c3eff3d9584f67651e2b98e58235afef2a8af2c0f99751c5df5ace +assets/assets/icon/bulk/home-dark.svg,1748498649545,05b56892212f1d3a1cf3163c96a81cc58ca957fe08f8021bae5f4c7edfa5a453 +assets/assets/icon/bulk/gift.svg,1748498649544,f86defe491d572eed84631ada71ebc955bcc8a52999e67a87322cd356f171621 +assets/assets/icon/bulk/chat.svg,1748498649543,652052e45085f4bd03b2d7116a821a9cb6223997541b38730cf2c07c927f1527 +assets/assets/icon/bulk/chat-dark.svg,1748498649542,783392715cc65cb59e41a347ac5520a127d6105b331c70992c1e6f03deb3f4ce +assets/assets/icon/bulk/characters.svg,1748498649541,c9cd537fbdb7303bc8e2695c2b228b5a8e3f8c301d69096937d0635fb3051aef +assets/assets/icon/bulk/characters-dark.svg,1748498649540,c9cd537fbdb7303bc8e2695c2b228b5a8e3f8c301d69096937d0635fb3051aef +assets/assets/icon/launcher_icons/houshan-icon-rounded.png,1745395920000,f732af36b1e26259c838a4a65e5f92a22eb5f1882ca70cd55ae2fec7ff6f802c +canvaskit/chromium/canvaskit.js.symbols,1744650586245,e3c8502ed70baa180726b7b0c9344c4a5409bf2722e0710bd55f2afd464246b5 +assets/assets/image/boardings/zwei.png,1745395920000,f4314fb4d17dd3f3e91b53d46ee9b5dd0d9a4170e5ea51556db99a4077dfcb20 +assets/assets/icon/bulk/camera.svg,1748498649538,73d7ce64d6869d0970514bbbbc6f18fe7eed5758be404598555cfd37914065c4 +assets/assets/icon/bulk/audio.svg,1748498649537,652aa3add6be8adbb16e1e7dcc8641d8bc6ac0d7c5c042a99bb970327452112f +assets/assets/icon/bulk/coin.png,1745395920000,eb79880b2444d15b987da040be16e271f5971a3196634056ee2f56e8f3c7cfed +assets/assets/icon/bulk/assistant.svg,1748498649536,daf2d037f914cb5e7f791bc8a372d251c1ef991424e9e433e057745822bcfd7d +assets/assets/icon/bulk/assistant-dark.svg,1748498649534,f6e86a6d5f1fe7278ec7c1bbb1d2583f552ac692e093b06e5875e3bf85b98fec +assets/assets/icon/bold/verify.svg,1748498649533,b69c2e431129f90b023b8f955dfb39461ff05032cda52ae9f626aee3ee05cd6c +assets/assets/icon/gif/heart.gif,1745395920000,142141f65915a1677ef298a01423bcca17ca4f67d8415a49c3b4ac78a331081c +assets/assets/icon/bold/stop.svg,1748498649531,657c996c5ae6988393340e3686637742ad5b7902038167c0ec2f9d6209907f2b +assets/assets/icon/bold/setting.svg,1748498649528,058d83aea00449646fdd4b530a7d32e6818181f4e239820134a3950f2221b2d0 +assets/assets/icon/bold/send.svg,1748498649524,89c14c915aaf2f7109a05721d5dc69f66808d6c1979ff7422ec052b5f33aa036 +assets/assets/icon/bold/profile.svg,1748498649524,468a1a8dd774291d2865b5139071d48029a5aa54bd14b29aef954ecdcb03af1a +assets/assets/icon/bold/play.svg,1748498649523,8089e3805ef043d55e0560616adb62c8e37268099f236538cf1d73d9453bd47f +canvaskit/canvaskit.js.symbols,1744650586213,dc06d86b568865a5ea79334b1fe84c435154802e8d6832818970eacf16d4c9f2 +assets/assets/icon/bold/pause.svg,1748498649522,223676b60b060da8ba585203f7a80d7e291d8991178f41487165ff1bc78a4eaa +assets/assets/icon/bold/my-assistant.svg,1748498649520,03131f584d486b588742a6a365e285c77df118014f797f979ecf0dfb33526e5c +assets/assets/icon/gif/clock.gif,1745395920000,32db2d59b63522c4002bf7e6d52d94476acb7d8926f97292f3fbabac08d91f54 +canvaskit/skwasm_st.js.symbols,1744650586305,67ce1c9ce2d7a402294908f738ca10d97167475bc71e94aef6fcb3f7b4aca9b5 +assets/assets/icon/bold/lock.svg,1748498649518,a7cfa63527e936c6c92abf6fea3db024371846dcdf40c87ce4f103447c015725 +assets/assets/icon/bold/like.svg,1748498649517,bfe862702a1b058f69259320b33bd6d8bac4882e3b55932f9e18ff24ec1be061 +assets/assets/icon/bold/key.svg,1748498649514,3cd84f456fc3a4ab6da6d7d32021858921200fcf5a5f33668241810bffa5abb4 +assets/assets/icon/bold/global-assistant.svg,1748498649512,72181b148ad818ca44bf2aacb11d56fcac34986b5968c87e494b55e665ed5f40 +assets/assets/icon/gif/bell.gif,1745395920000,dee86bad399a6cbce2083a3bf7f3a773ade8163d9343635715b64052c17f787e +assets/assets/icon/bold/dislike.svg,1748498649510,82be8965783351c41b2d6ff593aede084752e527d1a810a5837e7796a2eb7e99 +assets/assets/icon/bold/archive-tick.svg,1748498639909,ab7d78c337178bb2d6f3cb3cc367d1b805448dfa40ab049b7bea9c22846e9092 +assets/assets/icon/bold/create-assistant.svg,1748498649508,da2bd579fb844c981428e2685b0779129ded64e4ab04e165385fec81d0d4a998 +canvaskit/skwasm.js.symbols,1744650586278,bb8d3ba82c4539d4bfebbe8af9398cc5890bcca21f6acae3c00f8a6a1cacc814 +assets/assets/font/CustomIcons.ttf,1748779826856,2cfc96d98f6aff9b9691623f0df84678935ba257024f742d9301bcb45d82ea9f +.well_known/assetlinks.json,1748498714170,beb3d8bef89429407c40e82443b2ab6a7ec18d74667d348a53098939c8877f24 +assets/NOTICES,1748779822721,d4333ba29cba314758f31c87f496940ba7a56b221126f03e50e5e3a06c660195 +assets/assets/image/boardings/sechs.png,1745395920000,34606b72067aee8046a11bd54cb24d8af39da436214405c1a0ca609e1550fe59 +assets/assets/image/boardings/eins.png,1745395920000,6435b9bd60c23119418dd4eb8e20527e31a3187256a93fddc6bd0e92e34531a3 +assets/assets/font/IRANSansMobile-FaNum.ttf,1745395920000,1035e1196c149ce62450df727ecac5b5d7370f54b1aa4f5c62042e472670407a +assets/assets/icon/gif/instagram.gif,1745395920000,48b59d65ee9dc025aeddae4ab460f0ead6e4468493862a6efe6f654ff4f9df70 +assets/assets/image/boardings/vier.png,1745395920000,aff26b2dc768e23c667ef675815dd34cb80455870172190c28b529acebcca63a +assets/assets/icon/gif/medal.gif,1745395920000,2b37aaa02760f04b85b748c9f78d0f88d02bf6dbf6b20d59267670891310ec0b +assets/assets/image/boardings/funf.png,1745395920000,c9cd2d34e6e964ad652eb9f1500e9be28746fcfc546b6055cbe84054c03a41bd +assets/assets/image/boardings/drei.png,1745395920000,180f738e5cb56867035a11203272dbba056c131019d13ee2044d8312a74bf530 +assets/assets/icon/gif/coin.gif,1745395920000,830b5c4199aa0258c2683b30d2321f3b4bf4ea1608e51c2122f19e9c2862d4a9 +assets/assets/icon/gif/write.gif,1745395920000,542ad75fe5e242f0256aebb829cce2ac83fcf99e6f4fd6034548e1dc327c8725 +assets/assets/icon/gif/extras.gif,1745395920000,bac2d63e775ed8a5b43fb3aa8648e7b7c0b38dd2e8b704ed304e4309551947f3 +assets/assets/icon/gif/organizational.gif,1745395920000,059cf33b7abaece360c5371e00bf6a3af0e236daab7c93fc1c2e416c764743bc +assets/assets/image/image-g-back.jpg,1745395920000,87587bbff1f2833d830079800a4ab9e4d8e98362a1c8f5b5ed07cc3e9a7d3184 +assets/assets/icon/gif/wave_hand.gif,1745395920000,fb988f791bcc6400c67217546dce7ba98de9268b70377148cf4303f06798e1d0 +assets/assets/icon/gif/beta.gif,1745395920000,f4ac31dfd55a66b4dd08ae1a5220b04de2656ea00d333c100ecf4a62e38a84e8 +assets/assets/icon/gif/alpha.gif,1745395920000,5eb1ef562649a1da157d843b65500ff55c3b853cdc6561a9553c12166646ab1a +assets/assets/icon/gif/chat-main.gif,1745395920000,6b3e2e393b63409907dee2426320d842373b37eb0d776e72cd3162a198856ffa +assets/assets/icon/gif/one-coin.gif,1745395920000,3e06e722f2d03dbb3c199e54262f8dd6435ee07899837a43afe55acd700aa92f +assets/assets/icon/gif/delete.gif,1745395920000,9977e53ac21a750abe3f19a472231c338c1d945e14c8c342e27322080e5ce6e8 +assets/assets/icon/gif/gift.gif,1745395920000,fb9f0dd66ce89e2918fdf7197a9f5b96e04617f0566d75194ecfbbf489f03a94 +assets/assets/icon/gif/exit.gif,1745395920000,84ac7d00354fd22230bf1ab7c0fd1d2dc24d3c0a907635240a27be7d4ec0e5d1 +assets/assets/icon/gif/33.gif,1748504170397,982ffd9da1b08a8e30baae7847f28fcf49fdb55bf4cfa3494b1f5c90e207aafa +canvaskit/skwasm.wasm,1744650586300,f9dbb3615666b35096b3b3e5bb1f93a53e0742e8808e906cccb548a04ab53e2f +canvaskit/skwasm_st.wasm,1744650586306,b518709db54052c5efd05d0c64676d2159aaf35c69a4779f93727cfa63ee017e +canvaskit/chromium/canvaskit.wasm,1744650586262,829d9e6faa4899020801a7821358ee3e610630fc6f5a4fec0d2b5e9d8217a99c +canvaskit/canvaskit.wasm,1744650586245,dd73869f7a4737b9fbeb6c6acc8908dce896a285dcb111f7b55d753e07ae6be4 +main.dart.js,1748779820023,ab80cc3dc661c8fd5361bd7f6d2f91ce873f842f18140e8c13d9650a84c33a45 +web.zip,1748779863674,30fec16d4fbb6bb25f26c36a21b77ddfde9b05ff21a886af48f4c8968d8a8a1f diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000..1e70860 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "hoshan-42d9f" + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f4ac01f --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +linux +macos +windows +android/app/.cxx/Debug +android/app/.cxx/RelWithDebInfo + +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +!/android/app/src/main/kotlin/com/example/hoshan/FullscreenWebViewActivity.kt +/android/app/debug +/android/app/profile +/android/app/release + diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..aa90aa8 --- /dev/null +++ b/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ba393198430278b6595976de84fe170f553cc728" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: android + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: ios + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: linux + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: macos + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: web + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: windows + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8faa517 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive", + "cmake.ignoreCMakeListsMissing": true +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5211338 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# hoshan + +A powerfull ai Chat Application Based on Flutter. + +> [!TIP] +> Web Release link is : https://web.houshan.ai/ + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + +![hooshan banner](https://github.com/user-attachments/assets/0878023b-a349-4dfb-9e55-b0b5db477107) + +## important Commands: + +### Assets + +> for build icons and assets file +> +> ``` +> fluttergen +> ``` +> +> if not work or first time create project then use this and if warning you select option 1-Delete +> +> ``` +> dart run build_runner build +> ``` +> +> `flutter_gen:` [pub dev link](https://pub.dev/packages/flutter_gen) + +### App Icon + +> for build launcher icons +> +> ``` +> dart run flutter_launcher_icons +> ``` +> +> `flutter_launcher_icons:` [pub dev link](https://pub.dev/packages/flutter_launcher_icons) + +### App Name + +> for change app name +> +> ``` +> dart run rename_app:main all="My App Name" +> ``` +> +> `rename_app:` [pub dev link](https://pub.dev/packages/rename_app) + +### Android Release + +> for build android apk +> +> ``` +> flutter build apk +> ``` +> +> for split apks +> +> ``` +> flutter build apk --split-per-abi +> ``` + +### Web Release + +> for build web +> +> ``` +> flutter build web --web-renderer canvaskit +> ``` diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..6abe65d --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,31 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +analyzer: + errors: + depend_on_referenced_packages: ignore + deprecated_member_use_from_same_package: ignore +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..5748572 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,90 @@ +plugins { + id "com.android.application" + // START: FlutterFire Configuration + id 'com.google.gms.google-services' + // END: FlutterFire Configuration + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file("key.properties") +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + +android { + namespace "com.houshan.hoshan" + + compileSdk flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.houshan.hoshan" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion 24 + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword keystoreProperties['storePassword'] + } + } + + buildTypes { + release { + signingConfig signingConfigs.release + minifyEnabled false // Enable R8 for release builds + shrinkResources false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +flutter { + source '../..' +} + + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' +} + diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..90d63cd --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,106 @@ +{ + "project_info": { + "project_number": "581103504002", + "project_id": "hoshan-42d9f", + "storage_bucket": "hoshan-42d9f.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:581103504002:android:d12a150d3d54570418829b", + "android_client_info": { + "package_name": "com.example.hoshan" + } + }, + "oauth_client": [ + { + "client_id": "581103504002-kb0klg3vds4qcnt5cc348d402l803jh8.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.example.hoshan", + "certificate_hash": "4fea29f5e9872705f5d86af453b1d77fd4134260" + } + }, + { + "client_id": "581103504002-dhn875p11up9ae7k6l5r1kdqs71ap3qf.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDi2WRiOSEws1alpLitxX0zsX14rT71aPk" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "581103504002-dhn875p11up9ae7k6l5r1kdqs71ap3qf.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "581103504002-2e454pgr7fes6b94ptbt82rpfldehedq.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.example.hoshan" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:581103504002:android:896e4d41aad9180318829b", + "android_client_info": { + "package_name": "com.houshan.hoshan" + } + }, + "oauth_client": [ + { + "client_id": "581103504002-jqptfhs4cpgqngckh8g6kisd25jhfp35.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.houshan.hoshan", + "certificate_hash": "b64c77112af5000772d60fbe05138e873f6fc118" + } + }, + { + "client_id": "581103504002-t6a9nuq2nnbdjj71s2inc70p4fikl921.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.houshan.hoshan", + "certificate_hash": "5f7bdab9d1978fa6ba4bb89ba17cfddb41aa7bdb" + } + }, + { + "client_id": "581103504002-dhn875p11up9ae7k6l5r1kdqs71ap3qf.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDi2WRiOSEws1alpLitxX0zsX14rT71aPk" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "581103504002-dhn875p11up9ae7k6l5r1kdqs71ap3qf.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "581103504002-2e454pgr7fes6b94ptbt82rpfldehedq.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.example.hoshan" + } + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9a98aef --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/ic_launcher-playstore.png b/android/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..d78dad3 Binary files /dev/null and b/android/app/src/main/ic_launcher-playstore.png differ diff --git a/android/app/src/main/kotlin/com/example/hoshan/FullscreenWebViewActivity.kt b/android/app/src/main/kotlin/com/example/hoshan/FullscreenWebViewActivity.kt new file mode 100644 index 0000000..99c28bf --- /dev/null +++ b/android/app/src/main/kotlin/com/example/hoshan/FullscreenWebViewActivity.kt @@ -0,0 +1,104 @@ +package com.example.hoshan + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Bundle +import android.webkit.PermissionRequest +import android.webkit.WebChromeClient +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat + +class FullscreenWebViewActivity : AppCompatActivity() { + + private lateinit var webView: WebView + + private val PERMISSION_REQUEST_CODE = 1234 + private var pendingPermissionRequest: PermissionRequest? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + webView = WebView(this) + setContentView(webView) + + val url = intent.getStringExtra("url") ?: "https://www.google.com" + + val settings = webView.settings + settings.javaScriptEnabled = true + settings.mediaPlaybackRequiresUserGesture = false + settings.allowContentAccess = true + settings.allowFileAccess = true + settings.domStorageEnabled = true + settings.databaseEnabled = true + settings.cacheMode = WebSettings.LOAD_DEFAULT + + webView.webViewClient = WebViewClient() + + webView.webChromeClient = object : WebChromeClient() { + override fun onPermissionRequest(request: PermissionRequest?) { + runOnUiThread { + if (request == null) { + super.onPermissionRequest(request) + return@runOnUiThread + } + val requestedResources = request.resources + val permissionsNeeded = mutableListOf() + for (resource in requestedResources) { + when (resource) { + PermissionRequest.RESOURCE_VIDEO_CAPTURE -> { + if (ContextCompat.checkSelfPermission(this@FullscreenWebViewActivity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + permissionsNeeded.add(Manifest.permission.CAMERA) + } + } + PermissionRequest.RESOURCE_AUDIO_CAPTURE -> { + if (ContextCompat.checkSelfPermission(this@FullscreenWebViewActivity, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { + permissionsNeeded.add(Manifest.permission.RECORD_AUDIO) + } + } + } + } + + if (permissionsNeeded.isNotEmpty()) { + // درخواست پرمیژن runtime + pendingPermissionRequest = request + ActivityCompat.requestPermissions(this@FullscreenWebViewActivity, permissionsNeeded.toTypedArray(), PERMISSION_REQUEST_CODE) + } else { + // پرمیژن ها داده شده، قبول کن درخواست رو + request.grant(request.resources) + } + } + } + } + + webView.loadUrl(url) + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + if (requestCode == PERMISSION_REQUEST_CODE) { + var allGranted = true + for (result in grantResults) { + if (result != PackageManager.PERMISSION_GRANTED) { + allGranted = false + break + } + } + if (allGranted) { + pendingPermissionRequest?.grant(pendingPermissionRequest?.resources) + } else { + pendingPermissionRequest?.deny() + } + pendingPermissionRequest = null + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + } + + override fun onDestroy() { + super.onDestroy() + webView.destroy() + } +} diff --git a/android/app/src/main/kotlin/com/example/hoshan/MainActivity.kt b/android/app/src/main/kotlin/com/example/hoshan/MainActivity.kt new file mode 100644 index 0000000..29dd88b --- /dev/null +++ b/android/app/src/main/kotlin/com/example/hoshan/MainActivity.kt @@ -0,0 +1,76 @@ +package com.houshan.hoshan + +import io.flutter.embedding.android.FlutterActivity +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.os.Build +import android.provider.MediaStore +import android.util.Log +import androidx.annotation.NonNull +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import java.io.File +import java.io.FileInputStream +import java.io.OutputStream +import android.net.Uri +import androidx.core.content.FileProvider + +class MainActivity: FlutterActivity(){ + private val CHANNEL = "file_channel" + + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> + if (call.method == "copyToDownloads") { + val filePath = call.argument("filePath") ?: return@setMethodCallHandler + val fileName = call.argument("fileName") ?: return@setMethodCallHandler + + val success = copyFileToDownloads(filePath, fileName) + if (success) { + result.success("File copied successfully") + } else { + result.error("FILE_COPY_ERROR", "Failed to copy file", null) + } + } + } + + + } + + private fun copyFileToDownloads(filePath: String, fileName: String): Boolean { + return try { + val context: Context = applicationContext + val file = File(filePath) + if (!file.exists()) return false + + val resolver = context.contentResolver + val contentValues = ContentValues().apply { + put(MediaStore.Downloads.DISPLAY_NAME, fileName) + put(MediaStore.Downloads.MIME_TYPE, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") // Excel MIME type + put(MediaStore.Downloads.IS_PENDING, 1) + } + + val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) + ?: return false + + resolver.openOutputStream(uri).use { outputStream -> + FileInputStream(file).use { inputStream -> + inputStream.copyTo(outputStream!!) + } + } + + contentValues.clear() + contentValues.put(MediaStore.Downloads.IS_PENDING, 0) + resolver.update(uri, contentValues, null, null) + + true + } catch (e: Exception) { + Log.e("FileCopy", "Error copying file to downloads", e) + false + } + } + + +} diff --git a/android/app/src/main/res/drawable/hoshan_logo_unbackgrounded.png b/android/app/src/main/res/drawable/hoshan_logo_unbackgrounded.png new file mode 100644 index 0000000..2faa169 Binary files /dev/null and b/android/app/src/main/res/drawable/hoshan_logo_unbackgrounded.png differ diff --git a/android/app/src/main/res/drawable/ic_dialog_info.png b/android/app/src/main/res/drawable/ic_dialog_info.png new file mode 100644 index 0000000..2faa169 Binary files /dev/null and b/android/app/src/main/res/drawable/ic_dialog_info.png differ diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/rounded_rectangle.xml b/android/app/src/main/res/drawable/rounded_rectangle.xml new file mode 100644 index 0000000..4592b34 --- /dev/null +++ b/android/app/src/main/res/drawable/rounded_rectangle.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..9d7cb4e Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..cdf3ef6 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..d0cf996 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..ac651fb Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..67c0f7f Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..60b79ee Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..86364bf Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..edc3539 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..20f78b0 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..a677293 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..939e24c Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..58d06d3 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..a156a84 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..a3d218f Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..ef4374d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..a5e6970 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml new file mode 100644 index 0000000..a15aa52 --- /dev/null +++ b/android/app/src/main/res/values-v31/styles.xml @@ -0,0 +1,13 @@ + + + + diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..08dfd1a --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..864d596 --- /dev/null +++ b/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..ba2ea25 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,32 @@ +allprojects { + repositories { + maven { url "https://jitpack.io" } + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + // project.evaluationDependsOn(':app') + afterEvaluate { project -> + // check only for "com.android.library" to not modify + // your "app" subproject. All plugins will have "com.android.library" plugin, and only your app "com.android.application" + // Change your application's namespace in main build.gradle and in main android block. + + if (project.plugins.hasPlugin("com.android.library")) { + project.android { + if (namespace == null) { + namespace project.group + } + } + } + } +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..73c3168 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,6 @@ +org.gradle.jvmargs=-Xmx4G +android.useAndroidX=true +android.enableJetifier=true +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..208a26a --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Mar 25 09:42:09 IRST 2025 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..69dde9b --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,29 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() + + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version '8.3.2' apply false + // START: FlutterFire Configuration + id "com.google.gms.google-services" version "4.3.15" apply false + // END: FlutterFire Configuration + id "org.jetbrains.kotlin.android" version "1.9.20" apply false +} + +include ":app" diff --git a/assets/font/CustomIcons.ttf b/assets/font/CustomIcons.ttf new file mode 100644 index 0000000..6dc920f Binary files /dev/null and b/assets/font/CustomIcons.ttf differ diff --git a/assets/font/Dana-FaNum.ttf b/assets/font/Dana-FaNum.ttf new file mode 100644 index 0000000..3691b96 Binary files /dev/null and b/assets/font/Dana-FaNum.ttf differ diff --git a/assets/font/Dana.ttf b/assets/font/Dana.ttf new file mode 100644 index 0000000..93200a1 Binary files /dev/null and b/assets/font/Dana.ttf differ diff --git a/assets/font/IRANSansMobile-FaNum.ttf b/assets/font/IRANSansMobile-FaNum.ttf new file mode 100644 index 0000000..c60002a Binary files /dev/null and b/assets/font/IRANSansMobile-FaNum.ttf differ diff --git a/assets/font/IRANSansMobile.ttf b/assets/font/IRANSansMobile.ttf new file mode 100644 index 0000000..3c9b756 Binary files /dev/null and b/assets/font/IRANSansMobile.ttf differ diff --git a/assets/icon/bold/archive-tick.svg b/assets/icon/bold/archive-tick.svg new file mode 100644 index 0000000..8cc4fcb --- /dev/null +++ b/assets/icon/bold/archive-tick.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/bold/create-assistant.svg b/assets/icon/bold/create-assistant.svg new file mode 100644 index 0000000..f67d2e9 --- /dev/null +++ b/assets/icon/bold/create-assistant.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/bold/dislike.svg b/assets/icon/bold/dislike.svg new file mode 100644 index 0000000..fd5944f --- /dev/null +++ b/assets/icon/bold/dislike.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/bold/global-assistant.svg b/assets/icon/bold/global-assistant.svg new file mode 100644 index 0000000..c61b28f --- /dev/null +++ b/assets/icon/bold/global-assistant.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/bold/key.svg b/assets/icon/bold/key.svg new file mode 100644 index 0000000..dc6dabe --- /dev/null +++ b/assets/icon/bold/key.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/bold/like.svg b/assets/icon/bold/like.svg new file mode 100644 index 0000000..411f203 --- /dev/null +++ b/assets/icon/bold/like.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/bold/lock.svg b/assets/icon/bold/lock.svg new file mode 100644 index 0000000..c71dc3e --- /dev/null +++ b/assets/icon/bold/lock.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/bold/my-assistant.svg b/assets/icon/bold/my-assistant.svg new file mode 100644 index 0000000..cc481b2 --- /dev/null +++ b/assets/icon/bold/my-assistant.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/bold/pause.svg b/assets/icon/bold/pause.svg new file mode 100644 index 0000000..fdf1dc5 --- /dev/null +++ b/assets/icon/bold/pause.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/bold/play.svg b/assets/icon/bold/play.svg new file mode 100644 index 0000000..5a264b9 --- /dev/null +++ b/assets/icon/bold/play.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/bold/profile.svg b/assets/icon/bold/profile.svg new file mode 100644 index 0000000..8a2a85a --- /dev/null +++ b/assets/icon/bold/profile.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/bold/send.svg b/assets/icon/bold/send.svg new file mode 100644 index 0000000..9a4b7f5 --- /dev/null +++ b/assets/icon/bold/send.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/bold/setting.svg b/assets/icon/bold/setting.svg new file mode 100644 index 0000000..4312992 --- /dev/null +++ b/assets/icon/bold/setting.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/bold/stop.svg b/assets/icon/bold/stop.svg new file mode 100644 index 0000000..56f1960 --- /dev/null +++ b/assets/icon/bold/stop.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/bold/verify.svg b/assets/icon/bold/verify.svg new file mode 100644 index 0000000..0a39f1c --- /dev/null +++ b/assets/icon/bold/verify.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/bulk/assistant-dark.svg b/assets/icon/bulk/assistant-dark.svg new file mode 100644 index 0000000..b2193f2 --- /dev/null +++ b/assets/icon/bulk/assistant-dark.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icon/bulk/assistant.svg b/assets/icon/bulk/assistant.svg new file mode 100644 index 0000000..d210259 --- /dev/null +++ b/assets/icon/bulk/assistant.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icon/bulk/audio.svg b/assets/icon/bulk/audio.svg new file mode 100644 index 0000000..d4750dc --- /dev/null +++ b/assets/icon/bulk/audio.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/bulk/camera.svg b/assets/icon/bulk/camera.svg new file mode 100644 index 0000000..a1ec461 --- /dev/null +++ b/assets/icon/bulk/camera.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/bulk/characters-dark.svg b/assets/icon/bulk/characters-dark.svg new file mode 100644 index 0000000..0508946 --- /dev/null +++ b/assets/icon/bulk/characters-dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/bulk/characters.svg b/assets/icon/bulk/characters.svg new file mode 100644 index 0000000..0508946 --- /dev/null +++ b/assets/icon/bulk/characters.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/bulk/chat-dark.svg b/assets/icon/bulk/chat-dark.svg new file mode 100644 index 0000000..ce8ec45 --- /dev/null +++ b/assets/icon/bulk/chat-dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/bulk/chat.svg b/assets/icon/bulk/chat.svg new file mode 100644 index 0000000..c0ae1c5 --- /dev/null +++ b/assets/icon/bulk/chat.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/bulk/coin.png b/assets/icon/bulk/coin.png new file mode 100644 index 0000000..ccfc7b1 Binary files /dev/null and b/assets/icon/bulk/coin.png differ diff --git a/assets/icon/bulk/gift.svg b/assets/icon/bulk/gift.svg new file mode 100644 index 0000000..a9a69e7 --- /dev/null +++ b/assets/icon/bulk/gift.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/bulk/home-dark.svg b/assets/icon/bulk/home-dark.svg new file mode 100644 index 0000000..c4b083d --- /dev/null +++ b/assets/icon/bulk/home-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/bulk/home.svg b/assets/icon/bulk/home.svg new file mode 100644 index 0000000..7cfa4b7 --- /dev/null +++ b/assets/icon/bulk/home.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/bulk/library-dark.svg b/assets/icon/bulk/library-dark.svg new file mode 100644 index 0000000..8e460b1 --- /dev/null +++ b/assets/icon/bulk/library-dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/bulk/library.svg b/assets/icon/bulk/library.svg new file mode 100644 index 0000000..8b2f060 --- /dev/null +++ b/assets/icon/bulk/library.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/bulk/media-dark.svg b/assets/icon/bulk/media-dark.svg new file mode 100644 index 0000000..ec7dc96 --- /dev/null +++ b/assets/icon/bulk/media-dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/bulk/media.svg b/assets/icon/bulk/media.svg new file mode 100644 index 0000000..835cdc1 --- /dev/null +++ b/assets/icon/bulk/media.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/assets/icon/bulk/messages-dark.svg b/assets/icon/bulk/messages-dark.svg new file mode 100644 index 0000000..f560eee --- /dev/null +++ b/assets/icon/bulk/messages-dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/bulk/messages.svg b/assets/icon/bulk/messages.svg new file mode 100644 index 0000000..ad0f38a --- /dev/null +++ b/assets/icon/bulk/messages.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/bulk/news.svg b/assets/icon/bulk/news.svg new file mode 100644 index 0000000..96f11d9 --- /dev/null +++ b/assets/icon/bulk/news.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icon/bulk/setting-dark.svg b/assets/icon/bulk/setting-dark.svg new file mode 100644 index 0000000..ad2f9dd --- /dev/null +++ b/assets/icon/bulk/setting-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/bulk/setting.svg b/assets/icon/bulk/setting.svg new file mode 100644 index 0000000..f8f3ee2 --- /dev/null +++ b/assets/icon/bulk/setting.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/bulk/tool-box.svg b/assets/icon/bulk/tool-box.svg new file mode 100644 index 0000000..b21f7b9 --- /dev/null +++ b/assets/icon/bulk/tool-box.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/bulk/video.svg b/assets/icon/bulk/video.svg new file mode 100644 index 0000000..fc30460 --- /dev/null +++ b/assets/icon/bulk/video.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/bulk/warning-2.svg b/assets/icon/bulk/warning-2.svg new file mode 100644 index 0000000..ded0956 --- /dev/null +++ b/assets/icon/bulk/warning-2.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icon/gif/33.gif b/assets/icon/gif/33.gif new file mode 100644 index 0000000..5018089 Binary files /dev/null and b/assets/icon/gif/33.gif differ diff --git a/assets/icon/gif/alpha.gif b/assets/icon/gif/alpha.gif new file mode 100644 index 0000000..9475d9b Binary files /dev/null and b/assets/icon/gif/alpha.gif differ diff --git a/assets/icon/gif/bell.gif b/assets/icon/gif/bell.gif new file mode 100644 index 0000000..33c8a5c Binary files /dev/null and b/assets/icon/gif/bell.gif differ diff --git a/assets/icon/gif/beta.gif b/assets/icon/gif/beta.gif new file mode 100644 index 0000000..b29b5f0 Binary files /dev/null and b/assets/icon/gif/beta.gif differ diff --git a/assets/icon/gif/chat-main.gif b/assets/icon/gif/chat-main.gif new file mode 100644 index 0000000..1bc770f Binary files /dev/null and b/assets/icon/gif/chat-main.gif differ diff --git a/assets/icon/gif/clock.gif b/assets/icon/gif/clock.gif new file mode 100644 index 0000000..8ecf557 Binary files /dev/null and b/assets/icon/gif/clock.gif differ diff --git a/assets/icon/gif/coin.gif b/assets/icon/gif/coin.gif new file mode 100644 index 0000000..475119b Binary files /dev/null and b/assets/icon/gif/coin.gif differ diff --git a/assets/icon/gif/delete.gif b/assets/icon/gif/delete.gif new file mode 100644 index 0000000..b97fc94 Binary files /dev/null and b/assets/icon/gif/delete.gif differ diff --git a/assets/icon/gif/empty-bookmarks.gif b/assets/icon/gif/empty-bookmarks.gif new file mode 100644 index 0000000..ad80479 Binary files /dev/null and b/assets/icon/gif/empty-bookmarks.gif differ diff --git a/assets/icon/gif/exit.gif b/assets/icon/gif/exit.gif new file mode 100644 index 0000000..4367620 Binary files /dev/null and b/assets/icon/gif/exit.gif differ diff --git a/assets/icon/gif/extras.gif b/assets/icon/gif/extras.gif new file mode 100644 index 0000000..4925f77 Binary files /dev/null and b/assets/icon/gif/extras.gif differ diff --git a/assets/icon/gif/flash.gif b/assets/icon/gif/flash.gif new file mode 100644 index 0000000..5727359 Binary files /dev/null and b/assets/icon/gif/flash.gif differ diff --git a/assets/icon/gif/gift.gif b/assets/icon/gif/gift.gif new file mode 100644 index 0000000..8f67557 Binary files /dev/null and b/assets/icon/gif/gift.gif differ diff --git a/assets/icon/gif/heart.gif b/assets/icon/gif/heart.gif new file mode 100644 index 0000000..657c3cf Binary files /dev/null and b/assets/icon/gif/heart.gif differ diff --git a/assets/icon/gif/instagram.gif b/assets/icon/gif/instagram.gif new file mode 100644 index 0000000..dd87d5f Binary files /dev/null and b/assets/icon/gif/instagram.gif differ diff --git a/assets/icon/gif/medal.gif b/assets/icon/gif/medal.gif new file mode 100644 index 0000000..fda778d Binary files /dev/null and b/assets/icon/gif/medal.gif differ diff --git a/assets/icon/gif/one-coin.gif b/assets/icon/gif/one-coin.gif new file mode 100644 index 0000000..90ba7df Binary files /dev/null and b/assets/icon/gif/one-coin.gif differ diff --git a/assets/icon/gif/organizational.gif b/assets/icon/gif/organizational.gif new file mode 100644 index 0000000..40f7464 Binary files /dev/null and b/assets/icon/gif/organizational.gif differ diff --git a/assets/icon/gif/wave_hand.gif b/assets/icon/gif/wave_hand.gif new file mode 100644 index 0000000..994a2c2 Binary files /dev/null and b/assets/icon/gif/wave_hand.gif differ diff --git a/assets/icon/gif/write.gif b/assets/icon/gif/write.gif new file mode 100644 index 0000000..3496c5e Binary files /dev/null and b/assets/icon/gif/write.gif differ diff --git a/assets/icon/launcher_icons/houshan-icon-midround.png b/assets/icon/launcher_icons/houshan-icon-midround.png new file mode 100644 index 0000000..2381334 Binary files /dev/null and b/assets/icon/launcher_icons/houshan-icon-midround.png differ diff --git a/assets/icon/launcher_icons/houshan-icon-primary.png b/assets/icon/launcher_icons/houshan-icon-primary.png new file mode 100644 index 0000000..babf952 Binary files /dev/null and b/assets/icon/launcher_icons/houshan-icon-primary.png differ diff --git a/assets/icon/launcher_icons/houshan-icon-rounded.ico b/assets/icon/launcher_icons/houshan-icon-rounded.ico new file mode 100644 index 0000000..a05d422 Binary files /dev/null and b/assets/icon/launcher_icons/houshan-icon-rounded.ico differ diff --git a/assets/icon/launcher_icons/houshan-icon-rounded.png b/assets/icon/launcher_icons/houshan-icon-rounded.png new file mode 100644 index 0000000..6cbf036 Binary files /dev/null and b/assets/icon/launcher_icons/houshan-icon-rounded.png differ diff --git a/assets/icon/launcher_icons/houshan-icon-whie.png b/assets/icon/launcher_icons/houshan-icon-whie.png new file mode 100644 index 0000000..03a2370 Binary files /dev/null and b/assets/icon/launcher_icons/houshan-icon-whie.png differ diff --git a/assets/icon/launcher_icons/houshan-icon.png b/assets/icon/launcher_icons/houshan-icon.png new file mode 100644 index 0000000..2376eb7 Binary files /dev/null and b/assets/icon/launcher_icons/houshan-icon.png differ diff --git a/assets/icon/navbars/navigation-dark/assistant.svg b/assets/icon/navbars/navigation-dark/assistant.svg new file mode 100644 index 0000000..2fa0477 --- /dev/null +++ b/assets/icon/navbars/navigation-dark/assistant.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icon/navbars/navigation-dark/characters.svg b/assets/icon/navbars/navigation-dark/characters.svg new file mode 100644 index 0000000..f630e26 --- /dev/null +++ b/assets/icon/navbars/navigation-dark/characters.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/navbars/navigation-dark/home.svg b/assets/icon/navbars/navigation-dark/home.svg new file mode 100644 index 0000000..9a161ef --- /dev/null +++ b/assets/icon/navbars/navigation-dark/home.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/navbars/navigation-dark/media.svg b/assets/icon/navbars/navigation-dark/media.svg new file mode 100644 index 0000000..ec7dc96 --- /dev/null +++ b/assets/icon/navbars/navigation-dark/media.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/navbars/navigation-dark/setting.svg b/assets/icon/navbars/navigation-dark/setting.svg new file mode 100644 index 0000000..ad2f9dd --- /dev/null +++ b/assets/icon/navbars/navigation-dark/setting.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/navbars/navigation-light/assistant.svg b/assets/icon/navbars/navigation-light/assistant.svg new file mode 100644 index 0000000..3334bbd --- /dev/null +++ b/assets/icon/navbars/navigation-light/assistant.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icon/navbars/navigation-light/characters.svg b/assets/icon/navbars/navigation-light/characters.svg new file mode 100644 index 0000000..0508946 --- /dev/null +++ b/assets/icon/navbars/navigation-light/characters.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/navbars/navigation-light/home.svg b/assets/icon/navbars/navigation-light/home.svg new file mode 100644 index 0000000..7cfa4b7 --- /dev/null +++ b/assets/icon/navbars/navigation-light/home.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/navbars/navigation-light/media.svg b/assets/icon/navbars/navigation-light/media.svg new file mode 100644 index 0000000..5602868 --- /dev/null +++ b/assets/icon/navbars/navigation-light/media.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icon/navbars/navigation-light/setting.svg b/assets/icon/navbars/navigation-light/setting.svg new file mode 100644 index 0000000..f8f3ee2 --- /dev/null +++ b/assets/icon/navbars/navigation-light/setting.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/navbars/navigation/assistant.svg b/assets/icon/navbars/navigation/assistant.svg new file mode 100644 index 0000000..a330d16 --- /dev/null +++ b/assets/icon/navbars/navigation/assistant.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icon/navbars/navigation/characters.svg b/assets/icon/navbars/navigation/characters.svg new file mode 100644 index 0000000..1ae31bb --- /dev/null +++ b/assets/icon/navbars/navigation/characters.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/navbars/navigation/home.svg b/assets/icon/navbars/navigation/home.svg new file mode 100644 index 0000000..dedbbde --- /dev/null +++ b/assets/icon/navbars/navigation/home.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/navbars/navigation/media.svg b/assets/icon/navbars/navigation/media.svg new file mode 100644 index 0000000..25a471a --- /dev/null +++ b/assets/icon/navbars/navigation/media.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/navbars/navigation/setting.svg b/assets/icon/navbars/navigation/setting.svg new file mode 100644 index 0000000..8d3c315 --- /dev/null +++ b/assets/icon/navbars/navigation/setting.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/Houshan features.svg b/assets/icon/outline/Houshan features.svg new file mode 100644 index 0000000..baa822f --- /dev/null +++ b/assets/icon/outline/Houshan features.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/outline/add family.svg b/assets/icon/outline/add family.svg new file mode 100644 index 0000000..7842de4 --- /dev/null +++ b/assets/icon/outline/add family.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/outline/add.svg b/assets/icon/outline/add.svg new file mode 100644 index 0000000..b4db2d2 --- /dev/null +++ b/assets/icon/outline/add.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/archive-tick.svg b/assets/icon/outline/archive-tick.svg new file mode 100644 index 0000000..78c0eb3 --- /dev/null +++ b/assets/icon/outline/archive-tick.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/outline/arrow-flash-right.svg b/assets/icon/outline/arrow-flash-right.svg new file mode 100644 index 0000000..364e237 --- /dev/null +++ b/assets/icon/outline/arrow-flash-right.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/arrow-left.svg b/assets/icon/outline/arrow-left.svg new file mode 100644 index 0000000..2ea0e3f --- /dev/null +++ b/assets/icon/outline/arrow-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/outline/arrow-right.svg b/assets/icon/outline/arrow-right.svg new file mode 100644 index 0000000..aac6031 --- /dev/null +++ b/assets/icon/outline/arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/outline/assistant.svg b/assets/icon/outline/assistant.svg new file mode 100644 index 0000000..b7e2a37 --- /dev/null +++ b/assets/icon/outline/assistant.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icon/outline/bitcoin-refresh.svg b/assets/icon/outline/bitcoin-refresh.svg new file mode 100644 index 0000000..5ccb635 --- /dev/null +++ b/assets/icon/outline/bitcoin-refresh.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/outline/brain.svg b/assets/icon/outline/brain.svg new file mode 100644 index 0000000..6479b2e --- /dev/null +++ b/assets/icon/outline/brain.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/outline/brush.svg b/assets/icon/outline/brush.svg new file mode 100644 index 0000000..5ad323d --- /dev/null +++ b/assets/icon/outline/brush.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/outline/calendar-edit.svg b/assets/icon/outline/calendar-edit.svg new file mode 100644 index 0000000..5ac667e --- /dev/null +++ b/assets/icon/outline/calendar-edit.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icon/outline/call.svg b/assets/icon/outline/call.svg new file mode 100644 index 0000000..6f8bc94 --- /dev/null +++ b/assets/icon/outline/call.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/outline/camera.svg b/assets/icon/outline/camera.svg new file mode 100644 index 0000000..66deee8 --- /dev/null +++ b/assets/icon/outline/camera.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icon/outline/carbon_pedestrian-family.svg b/assets/icon/outline/carbon_pedestrian-family.svg new file mode 100644 index 0000000..359862f --- /dev/null +++ b/assets/icon/outline/carbon_pedestrian-family.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/card-add.svg b/assets/icon/outline/card-add.svg new file mode 100644 index 0000000..1e207e8 --- /dev/null +++ b/assets/icon/outline/card-add.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icon/outline/card-pos.svg b/assets/icon/outline/card-pos.svg new file mode 100644 index 0000000..80a35b1 --- /dev/null +++ b/assets/icon/outline/card-pos.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/outline/characters.svg b/assets/icon/outline/characters.svg new file mode 100644 index 0000000..1074762 --- /dev/null +++ b/assets/icon/outline/characters.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/outline/chart.svg b/assets/icon/outline/chart.svg new file mode 100644 index 0000000..70fbc19 --- /dev/null +++ b/assets/icon/outline/chart.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/outline/chat.svg b/assets/icon/outline/chat.svg new file mode 100644 index 0000000..aef7740 --- /dev/null +++ b/assets/icon/outline/chat.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icon/outline/clock.svg b/assets/icon/outline/clock.svg new file mode 100644 index 0000000..7ced0d7 --- /dev/null +++ b/assets/icon/outline/clock.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/close-circle.svg b/assets/icon/outline/close-circle.svg new file mode 100644 index 0000000..ddc20c3 --- /dev/null +++ b/assets/icon/outline/close-circle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icon/outline/coin.svg b/assets/icon/outline/coin.svg new file mode 100644 index 0000000..5d51909 --- /dev/null +++ b/assets/icon/outline/coin.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icon/outline/copy.svg b/assets/icon/outline/copy.svg new file mode 100644 index 0000000..0f1d9c9 --- /dev/null +++ b/assets/icon/outline/copy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/courthouse.svg b/assets/icon/outline/courthouse.svg new file mode 100644 index 0000000..8dea531 --- /dev/null +++ b/assets/icon/outline/courthouse.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icon/outline/crown.svg b/assets/icon/outline/crown.svg new file mode 100644 index 0000000..7c6a6c3 --- /dev/null +++ b/assets/icon/outline/crown.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icon/outline/direct-inbox.svg b/assets/icon/outline/direct-inbox.svg new file mode 100644 index 0000000..221fd17 --- /dev/null +++ b/assets/icon/outline/direct-inbox.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/outline/direct-send.svg b/assets/icon/outline/direct-send.svg new file mode 100644 index 0000000..bf24781 --- /dev/null +++ b/assets/icon/outline/direct-send.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/outline/dislike.svg b/assets/icon/outline/dislike.svg new file mode 100644 index 0000000..6114fbc --- /dev/null +++ b/assets/icon/outline/dislike.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/document-copy.svg b/assets/icon/outline/document-copy.svg new file mode 100644 index 0000000..dbd7fa1 --- /dev/null +++ b/assets/icon/outline/document-copy.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icon/outline/document-text.svg b/assets/icon/outline/document-text.svg new file mode 100644 index 0000000..b7097cd --- /dev/null +++ b/assets/icon/outline/document-text.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/outline/download.svg b/assets/icon/outline/download.svg new file mode 100644 index 0000000..676a9cd --- /dev/null +++ b/assets/icon/outline/download.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icon/outline/edit-2.svg b/assets/icon/outline/edit-2.svg new file mode 100644 index 0000000..f9c99c8 --- /dev/null +++ b/assets/icon/outline/edit-2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/edit.svg b/assets/icon/outline/edit.svg new file mode 100644 index 0000000..051a35d --- /dev/null +++ b/assets/icon/outline/edit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/element-plus.svg b/assets/icon/outline/element-plus.svg new file mode 100644 index 0000000..56f5b85 --- /dev/null +++ b/assets/icon/outline/element-plus.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/outline/emoji-happy.svg b/assets/icon/outline/emoji-happy.svg new file mode 100644 index 0000000..110f111 --- /dev/null +++ b/assets/icon/outline/emoji-happy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/outline/empty-wallet-tick.svg b/assets/icon/outline/empty-wallet-tick.svg new file mode 100644 index 0000000..a1f9bac --- /dev/null +++ b/assets/icon/outline/empty-wallet-tick.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icon/outline/empty-wallet.svg b/assets/icon/outline/empty-wallet.svg new file mode 100644 index 0000000..59ff7f0 --- /dev/null +++ b/assets/icon/outline/empty-wallet.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/outline/eraser.svg b/assets/icon/outline/eraser.svg new file mode 100644 index 0000000..5f1580c --- /dev/null +++ b/assets/icon/outline/eraser.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/outline/family.svg b/assets/icon/outline/family.svg new file mode 100644 index 0000000..dcf74f0 --- /dev/null +++ b/assets/icon/outline/family.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icon/outline/familyMembers.svg b/assets/icon/outline/familyMembers.svg new file mode 100644 index 0000000..975fe26 --- /dev/null +++ b/assets/icon/outline/familyMembers.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/outline/filter.svg b/assets/icon/outline/filter.svg new file mode 100644 index 0000000..7c2ef6e --- /dev/null +++ b/assets/icon/outline/filter.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/flag-2.svg b/assets/icon/outline/flag-2.svg new file mode 100644 index 0000000..6d19260 --- /dev/null +++ b/assets/icon/outline/flag-2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/gallery-add.svg b/assets/icon/outline/gallery-add.svg new file mode 100644 index 0000000..1e587a3 --- /dev/null +++ b/assets/icon/outline/gallery-add.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/outline/ghost.svg b/assets/icon/outline/ghost.svg new file mode 100644 index 0000000..514bea3 --- /dev/null +++ b/assets/icon/outline/ghost.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/outline/gift 2.svg b/assets/icon/outline/gift 2.svg new file mode 100644 index 0000000..439098a --- /dev/null +++ b/assets/icon/outline/gift 2.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icon/outline/gift.svg b/assets/icon/outline/gift.svg new file mode 100644 index 0000000..85b38d7 --- /dev/null +++ b/assets/icon/outline/gift.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/outline/global-search.svg b/assets/icon/outline/global-search.svg new file mode 100644 index 0000000..91c3317 --- /dev/null +++ b/assets/icon/outline/global-search.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icon/outline/hat.svg b/assets/icon/outline/hat.svg new file mode 100644 index 0000000..805f6b4 --- /dev/null +++ b/assets/icon/outline/hat.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/outline/heart.svg b/assets/icon/outline/heart.svg new file mode 100644 index 0000000..0fb9b4e --- /dev/null +++ b/assets/icon/outline/heart.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/outline/home.svg b/assets/icon/outline/home.svg new file mode 100644 index 0000000..8fff4a3 --- /dev/null +++ b/assets/icon/outline/home.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/idea.svg b/assets/icon/outline/idea.svg new file mode 100644 index 0000000..085ef8b --- /dev/null +++ b/assets/icon/outline/idea.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/outline/import.svg b/assets/icon/outline/import.svg new file mode 100644 index 0000000..32bb6d5 --- /dev/null +++ b/assets/icon/outline/import.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icon/outline/info-circle.svg b/assets/icon/outline/info-circle.svg new file mode 100644 index 0000000..e7220ed --- /dev/null +++ b/assets/icon/outline/info-circle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icon/outline/ion_infinite.svg b/assets/icon/outline/ion_infinite.svg new file mode 100644 index 0000000..da3811c --- /dev/null +++ b/assets/icon/outline/ion_infinite.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/outline/lamp-charge.svg b/assets/icon/outline/lamp-charge.svg new file mode 100644 index 0000000..446acea --- /dev/null +++ b/assets/icon/outline/lamp-charge.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icon/outline/library.svg b/assets/icon/outline/library.svg new file mode 100644 index 0000000..d97093a --- /dev/null +++ b/assets/icon/outline/library.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/outline/like.svg b/assets/icon/outline/like.svg new file mode 100644 index 0000000..f8c19a7 --- /dev/null +++ b/assets/icon/outline/like.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/lock.svg b/assets/icon/outline/lock.svg new file mode 100644 index 0000000..15fee38 --- /dev/null +++ b/assets/icon/outline/lock.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/outline/login.svg b/assets/icon/outline/login.svg new file mode 100644 index 0000000..bc495fe --- /dev/null +++ b/assets/icon/outline/login.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icon/outline/mage_scan-user.svg b/assets/icon/outline/mage_scan-user.svg new file mode 100644 index 0000000..8d6ae42 --- /dev/null +++ b/assets/icon/outline/mage_scan-user.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/magicpen.svg b/assets/icon/outline/magicpen.svg new file mode 100644 index 0000000..1534e98 --- /dev/null +++ b/assets/icon/outline/magicpen.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/outline/media.svg b/assets/icon/outline/media.svg new file mode 100644 index 0000000..80217ab --- /dev/null +++ b/assets/icon/outline/media.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/message-question.svg b/assets/icon/outline/message-question.svg new file mode 100644 index 0000000..7387998 --- /dev/null +++ b/assets/icon/outline/message-question.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icon/outline/message-text.svg b/assets/icon/outline/message-text.svg new file mode 100644 index 0000000..373f7a8 --- /dev/null +++ b/assets/icon/outline/message-text.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icon/outline/messages.svg b/assets/icon/outline/messages.svg new file mode 100644 index 0000000..e0a04d2 --- /dev/null +++ b/assets/icon/outline/messages.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/outline/microphone-chat.svg b/assets/icon/outline/microphone-chat.svg new file mode 100644 index 0000000..90bae28 --- /dev/null +++ b/assets/icon/outline/microphone-chat.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/outline/mobile.svg b/assets/icon/outline/mobile.svg new file mode 100644 index 0000000..c16de2f --- /dev/null +++ b/assets/icon/outline/mobile.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icon/outline/moon.svg b/assets/icon/outline/moon.svg new file mode 100644 index 0000000..d4907c7 --- /dev/null +++ b/assets/icon/outline/moon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/outline/more.svg b/assets/icon/outline/more.svg new file mode 100644 index 0000000..37ec653 --- /dev/null +++ b/assets/icon/outline/more.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icon/outline/musicnote.svg b/assets/icon/outline/musicnote.svg new file mode 100644 index 0000000..dd5b061 --- /dev/null +++ b/assets/icon/outline/musicnote.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icon/outline/news.svg b/assets/icon/outline/news.svg new file mode 100644 index 0000000..728f655 --- /dev/null +++ b/assets/icon/outline/news.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icon/outline/notification-bing.svg b/assets/icon/outline/notification-bing.svg new file mode 100644 index 0000000..ce4928b --- /dev/null +++ b/assets/icon/outline/notification-bing.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icon/outline/pause.svg b/assets/icon/outline/pause.svg new file mode 100644 index 0000000..9bea833 --- /dev/null +++ b/assets/icon/outline/pause.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/play.svg b/assets/icon/outline/play.svg new file mode 100644 index 0000000..4102939 --- /dev/null +++ b/assets/icon/outline/play.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/outline/profile-tick.svg b/assets/icon/outline/profile-tick.svg new file mode 100644 index 0000000..5c7d238 --- /dev/null +++ b/assets/icon/outline/profile-tick.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icon/outline/profile-user-doual.svg b/assets/icon/outline/profile-user-doual.svg new file mode 100644 index 0000000..6ce0237 --- /dev/null +++ b/assets/icon/outline/profile-user-doual.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/outline/profile.svg b/assets/icon/outline/profile.svg new file mode 100644 index 0000000..5805ad3 --- /dev/null +++ b/assets/icon/outline/profile.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/search-normal.svg b/assets/icon/outline/search-normal.svg new file mode 100644 index 0000000..27eb7ab --- /dev/null +++ b/assets/icon/outline/search-normal.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/send.svg b/assets/icon/outline/send.svg new file mode 100644 index 0000000..d31cdbc --- /dev/null +++ b/assets/icon/outline/send.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/outline/setting.svg b/assets/icon/outline/setting.svg new file mode 100644 index 0000000..8d3c315 --- /dev/null +++ b/assets/icon/outline/setting.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/share.svg b/assets/icon/outline/share.svg new file mode 100644 index 0000000..69cbb46 --- /dev/null +++ b/assets/icon/outline/share.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icon/outline/shield-tick.svg b/assets/icon/outline/shield-tick.svg new file mode 100644 index 0000000..e5c97ed --- /dev/null +++ b/assets/icon/outline/shield-tick.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/sms-tracking.svg b/assets/icon/outline/sms-tracking.svg new file mode 100644 index 0000000..6aca19b --- /dev/null +++ b/assets/icon/outline/sms-tracking.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/outline/stop-circle.svg b/assets/icon/outline/stop-circle.svg new file mode 100644 index 0000000..54b347a --- /dev/null +++ b/assets/icon/outline/stop-circle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/stop.svg b/assets/icon/outline/stop.svg new file mode 100644 index 0000000..d94b537 --- /dev/null +++ b/assets/icon/outline/stop.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/outline/streamline_coins-stack.svg b/assets/icon/outline/streamline_coins-stack.svg new file mode 100644 index 0000000..ae761c5 --- /dev/null +++ b/assets/icon/outline/streamline_coins-stack.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icon/outline/sun.svg b/assets/icon/outline/sun.svg new file mode 100644 index 0000000..ffbffa7 --- /dev/null +++ b/assets/icon/outline/sun.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icon/outline/tick-circle.svg b/assets/icon/outline/tick-circle.svg new file mode 100644 index 0000000..48467f0 --- /dev/null +++ b/assets/icon/outline/tick-circle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/tick-square.svg b/assets/icon/outline/tick-square.svg new file mode 100644 index 0000000..751d3f2 --- /dev/null +++ b/assets/icon/outline/tick-square.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/timer.svg b/assets/icon/outline/timer.svg new file mode 100644 index 0000000..4e6255c --- /dev/null +++ b/assets/icon/outline/timer.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/outline/tool-box.svg b/assets/icon/outline/tool-box.svg new file mode 100644 index 0000000..d643cb0 --- /dev/null +++ b/assets/icon/outline/tool-box.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icon/outline/translate.svg b/assets/icon/outline/translate.svg new file mode 100644 index 0000000..7eb635a --- /dev/null +++ b/assets/icon/outline/translate.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icon/outline/trash.svg b/assets/icon/outline/trash.svg new file mode 100644 index 0000000..f835806 --- /dev/null +++ b/assets/icon/outline/trash.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/outline/trashPopup.svg b/assets/icon/outline/trashPopup.svg new file mode 100644 index 0000000..3009e08 --- /dev/null +++ b/assets/icon/outline/trashPopup.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icon/outline/verify.svg b/assets/icon/outline/verify.svg new file mode 100644 index 0000000..1c38b03 --- /dev/null +++ b/assets/icon/outline/verify.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon/outline/video-play.svg b/assets/icon/outline/video-play.svg new file mode 100644 index 0000000..9217653 --- /dev/null +++ b/assets/icon/outline/video-play.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/outline/voice-cricle.svg b/assets/icon/outline/voice-cricle.svg new file mode 100644 index 0000000..74a7b6d --- /dev/null +++ b/assets/icon/outline/voice-cricle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/outline/wallet.svg b/assets/icon/outline/wallet.svg new file mode 100644 index 0000000..b25cdd8 --- /dev/null +++ b/assets/icon/outline/wallet.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icon/outline/warning-2.svg b/assets/icon/outline/warning-2.svg new file mode 100644 index 0000000..925a4fb --- /dev/null +++ b/assets/icon/outline/warning-2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icon/outline/warning.svg b/assets/icon/outline/warning.svg new file mode 100644 index 0000000..f5544e3 --- /dev/null +++ b/assets/icon/outline/warning.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/signin/igoogle.svg b/assets/icon/signin/igoogle.svg new file mode 100644 index 0000000..d61ab94 --- /dev/null +++ b/assets/icon/signin/igoogle.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icon/social/bold/instagram.svg b/assets/icon/social/bold/instagram.svg new file mode 100644 index 0000000..a5d18c0 --- /dev/null +++ b/assets/icon/social/bold/instagram.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon/social/bold/linkdin.svg b/assets/icon/social/bold/linkdin.svg new file mode 100644 index 0000000..9d832ad --- /dev/null +++ b/assets/icon/social/bold/linkdin.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icon/social/bold/site.svg b/assets/icon/social/bold/site.svg new file mode 100644 index 0000000..a222694 --- /dev/null +++ b/assets/icon/social/bold/site.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icon/social/bold/telegram.svg b/assets/icon/social/bold/telegram.svg new file mode 100644 index 0000000..5ca24db --- /dev/null +++ b/assets/icon/social/bold/telegram.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icon/social/bold/twitter.svg b/assets/icon/social/bold/twitter.svg new file mode 100644 index 0000000..9cd1cf6 --- /dev/null +++ b/assets/icon/social/bold/twitter.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icon/social/bold/youtube.svg b/assets/icon/social/bold/youtube.svg new file mode 100644 index 0000000..903e6f7 --- /dev/null +++ b/assets/icon/social/bold/youtube.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/image/app-icon-primary.svg b/assets/image/app-icon-primary.svg new file mode 100644 index 0000000..b5cb95d --- /dev/null +++ b/assets/image/app-icon-primary.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/image/app-icon.svg b/assets/image/app-icon.svg new file mode 100644 index 0000000..2716c51 --- /dev/null +++ b/assets/image/app-icon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/image/audio-back.png b/assets/image/audio-back.png new file mode 100644 index 0000000..c763b3e Binary files /dev/null and b/assets/image/audio-back.png differ diff --git a/assets/image/boardings/AI Houshan.svg b/assets/image/boardings/AI Houshan.svg new file mode 100644 index 0000000..f2fa0e6 --- /dev/null +++ b/assets/image/boardings/AI Houshan.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/image/boardings/drei.png b/assets/image/boardings/drei.png new file mode 100644 index 0000000..f998971 Binary files /dev/null and b/assets/image/boardings/drei.png differ diff --git a/assets/image/boardings/eins.png b/assets/image/boardings/eins.png new file mode 100644 index 0000000..d1b4077 Binary files /dev/null and b/assets/image/boardings/eins.png differ diff --git a/assets/image/boardings/funf.png b/assets/image/boardings/funf.png new file mode 100644 index 0000000..5f50de3 Binary files /dev/null and b/assets/image/boardings/funf.png differ diff --git a/assets/image/boardings/sechs.png b/assets/image/boardings/sechs.png new file mode 100644 index 0000000..263ebc8 Binary files /dev/null and b/assets/image/boardings/sechs.png differ diff --git a/assets/image/boardings/vier.png b/assets/image/boardings/vier.png new file mode 100644 index 0000000..dacd4bb Binary files /dev/null and b/assets/image/boardings/vier.png differ diff --git a/assets/image/boardings/zwei.png b/assets/image/boardings/zwei.png new file mode 100644 index 0000000..98f9f89 Binary files /dev/null and b/assets/image/boardings/zwei.png differ diff --git a/assets/image/chat-back.png b/assets/image/chat-back.png new file mode 100644 index 0000000..d1fcbd0 Binary files /dev/null and b/assets/image/chat-back.png differ diff --git a/assets/image/empty/amount.png b/assets/image/empty/amount.png new file mode 100644 index 0000000..b3a25d8 Binary files /dev/null and b/assets/image/empty/amount.png differ diff --git a/assets/image/empty/assistant.png b/assets/image/empty/assistant.png new file mode 100644 index 0000000..37f8a75 Binary files /dev/null and b/assets/image/empty/assistant.png differ diff --git a/assets/image/empty/connection.png b/assets/image/empty/connection.png new file mode 100644 index 0000000..4077014 Binary files /dev/null and b/assets/image/empty/connection.png differ diff --git a/assets/image/empty/empty state 1 1.png b/assets/image/empty/empty state 1 1.png new file mode 100644 index 0000000..2f56162 Binary files /dev/null and b/assets/image/empty/empty state 1 1.png differ diff --git a/assets/image/empty/empty-text-underline.svg b/assets/image/empty/empty-text-underline.svg new file mode 100644 index 0000000..0cc304e --- /dev/null +++ b/assets/image/empty/empty-text-underline.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/image/empty/inbox.png b/assets/image/empty/inbox.png new file mode 100644 index 0000000..e19d36b Binary files /dev/null and b/assets/image/empty/inbox.png differ diff --git a/assets/image/empty/messages.png b/assets/image/empty/messages.png new file mode 100644 index 0000000..15c3c8a Binary files /dev/null and b/assets/image/empty/messages.png differ diff --git a/assets/image/empty/reverse-arrow.png b/assets/image/empty/reverse-arrow.png new file mode 100644 index 0000000..8d2ffc2 Binary files /dev/null and b/assets/image/empty/reverse-arrow.png differ diff --git a/assets/image/empty/server.png b/assets/image/empty/server.png new file mode 100644 index 0000000..99c2321 Binary files /dev/null and b/assets/image/empty/server.png differ diff --git a/assets/image/expected_format.png b/assets/image/expected_format.png new file mode 100644 index 0000000..1eb9885 Binary files /dev/null and b/assets/image/expected_format.png differ diff --git a/assets/image/image-g-back.jpg b/assets/image/image-g-back.jpg new file mode 100644 index 0000000..c4fe6b4 Binary files /dev/null and b/assets/image/image-g-back.jpg differ diff --git a/assets/image/income-steps.png b/assets/image/income-steps.png new file mode 100644 index 0000000..4a8ad93 Binary files /dev/null and b/assets/image/income-steps.png differ diff --git a/assets/image/splash/splash-desk.png b/assets/image/splash/splash-desk.png new file mode 100644 index 0000000..ad0ea9b Binary files /dev/null and b/assets/image/splash/splash-desk.png differ diff --git a/assets/image/splash/splash.png b/assets/image/splash/splash.png new file mode 100644 index 0000000..b60cba4 Binary files /dev/null and b/assets/image/splash/splash.png differ diff --git a/assets/image/video-back.png b/assets/image/video-back.png new file mode 100644 index 0000000..be3b7a8 Binary files /dev/null and b/assets/image/video-back.png differ diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..7e7e7f6 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/enabled.json b/enabled.json new file mode 100644 index 0000000..27ba77d --- /dev/null +++ b/enabled.json @@ -0,0 +1 @@ +true diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..6cc45c9 --- /dev/null +++ b/firebase.json @@ -0,0 +1,39 @@ +{ + "flutter": { + "platforms": { + "android": { + "default": { + "projectId": "hoshan-42d9f", + "appId": "1:581103504002:android:d12a150d3d54570418829b", + "fileOutput": "android/app/google-services.json" + } + }, + "dart": { + "lib/firebase_options.dart": { + "projectId": "hoshan-42d9f", + "configurations": { + "android": "1:581103504002:android:d12a150d3d54570418829b", + "ios": "1:581103504002:ios:4e51253f750a0fc818829b", + "macos": "1:581103504002:ios:4e51253f750a0fc818829b", + "web": "1:581103504002:web:8facd97674b83ac218829b", + "windows": "1:581103504002:web:084a33707f94511118829b" + } + } + } + } + }, + "hosting": { + "public": "build/web", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + } +} diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..e549ee2 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..7b322be --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.houshan.hoshan; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.houshan.hoshan.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.houshan.hoshan.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.houshan.hoshan.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.houshan.hoshan; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.houshan.hoshan; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..8e3ca5d --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d0d98aa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..e983628 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..5a8ea6b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..50e1bc9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..fb13060 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..ae2fca8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..b788297 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..503f20e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..50e1bc9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..3c3a0cb Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..fe5882f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000..7754002 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000..ef68d03 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000..c88df96 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000..6531382 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..fe5882f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..80080d9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000..3616785 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000..d139659 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..82ce1b2 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..3df7632 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..f0d66d4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..3928b55 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,65 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Chat + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + hoshan + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + CFBundleURLTypes + + + CFBundleURLSchemes + + YOUR_REVERSED_CLIENT_ID + houshan + + + + NSPhotoLibraryUsageDescription + This app requires to save your images user gallery + NSCameraUsageDescription + We need access to your camera to take photos. + NSPhotoLibraryUsageDescription + We need access to your photo library to save photos. + + \ No newline at end of file diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/lib/core/gen/assets.gen.dart b/lib/core/gen/assets.gen.dart new file mode 100644 index 0000000..f453401 --- /dev/null +++ b/lib/core/gen/assets.gen.dart @@ -0,0 +1,1564 @@ +/// GENERATED CODE - DO NOT MODIFY BY HAND +/// ***************************************************** +/// FlutterGen +/// ***************************************************** + +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_svg/flutter_svg.dart' as _svg; +import 'package:vector_graphics/vector_graphics.dart' as _vg; + +class $AssetsIconGen { + const $AssetsIconGen(); + + /// Directory path: assets/icon/bold + $AssetsIconBoldGen get bold => const $AssetsIconBoldGen(); + + /// Directory path: assets/icon/bulk + $AssetsIconBulkGen get bulk => const $AssetsIconBulkGen(); + + /// Directory path: assets/icon/gif + $AssetsIconGifGen get gif => const $AssetsIconGifGen(); + + /// Directory path: assets/icon/launcher_icons + $AssetsIconLauncherIconsGen get launcherIcons => + const $AssetsIconLauncherIconsGen(); + + /// Directory path: assets/icon/navbars + $AssetsIconNavbarsGen get navbars => const $AssetsIconNavbarsGen(); + + /// Directory path: assets/icon/outline + $AssetsIconOutlineGen get outline => const $AssetsIconOutlineGen(); + + /// Directory path: assets/icon/signin + $AssetsIconSigninGen get signin => const $AssetsIconSigninGen(); + + /// Directory path: assets/icon/social + $AssetsIconSocialGen get social => const $AssetsIconSocialGen(); +} + +class $AssetsImageGen { + const $AssetsImageGen(); + + /// File path: assets/image/app-icon-primary.svg + SvgGenImage get appIconPrimary => const SvgGenImage( + 'assets/image/app-icon-primary.svg', + size: Size(81.0, 39.0), + ); + + /// File path: assets/image/app-icon.svg + SvgGenImage get appIcon => + const SvgGenImage('assets/image/app-icon.svg', size: Size(138.0, 68.0)); + + /// File path: assets/image/audio-back.png + AssetGenImage get audioBack => const AssetGenImage( + 'assets/image/audio-back.png', + size: Size(1080.0, 2214.0), + ); + + /// Directory path: assets/image/boardings + $AssetsImageBoardingsGen get boardings => const $AssetsImageBoardingsGen(); + + /// File path: assets/image/chat-back.png + AssetGenImage get chatBack => const AssetGenImage( + 'assets/image/chat-back.png', + size: Size(360.0, 738.0), + ); + + /// Directory path: assets/image/empty + $AssetsImageEmptyGen get empty => const $AssetsImageEmptyGen(); + + /// File path: assets/image/expected_format.png + AssetGenImage get expectedFormat => const AssetGenImage( + 'assets/image/expected_format.png', + size: Size(1312.0, 244.0), + ); + + /// File path: assets/image/image-g-back.jpg + AssetGenImage get imageGBack => const AssetGenImage( + 'assets/image/image-g-back.jpg', + size: Size(2000.0, 2000.0), + ); + + /// File path: assets/image/income-steps.png + AssetGenImage get incomeSteps => const AssetGenImage( + 'assets/image/income-steps.png', + size: Size(328.0, 362.0), + ); + + /// Directory path: assets/image/splash + $AssetsImageSplashGen get splash => const $AssetsImageSplashGen(); + + /// File path: assets/image/video-back.png + AssetGenImage get videoBack => const AssetGenImage( + 'assets/image/video-back.png', + size: Size(360.0, 738.0), + ); + + /// List of all assets + List get values => [ + appIconPrimary, + appIcon, + audioBack, + chatBack, + expectedFormat, + imageGBack, + incomeSteps, + videoBack, + ]; +} + +class $AssetsIconBoldGen { + const $AssetsIconBoldGen(); + + /// File path: assets/icon/bold/archive-tick.svg + SvgGenImage get archiveTick => const SvgGenImage( + 'assets/icon/bold/archive-tick.svg', + size: Size(16.0, 19.0), + ); + + /// File path: assets/icon/bold/create-assistant.svg + SvgGenImage get createAssistant => const SvgGenImage( + 'assets/icon/bold/create-assistant.svg', + size: Size(26.0, 28.0), + ); + + /// File path: assets/icon/bold/dislike.svg + SvgGenImage get dislike => + const SvgGenImage('assets/icon/bold/dislike.svg', size: Size(24.0, 24.0)); + + /// File path: assets/icon/bold/global-assistant.svg + SvgGenImage get globalAssistant => const SvgGenImage( + 'assets/icon/bold/global-assistant.svg', + size: Size(27.0, 29.0), + ); + + /// File path: assets/icon/bold/key.svg + SvgGenImage get key => + const SvgGenImage('assets/icon/bold/key.svg', size: Size(19.0, 19.0)); + + /// File path: assets/icon/bold/like.svg + SvgGenImage get like => + const SvgGenImage('assets/icon/bold/like.svg', size: Size(24.0, 24.0)); + + /// File path: assets/icon/bold/lock.svg + SvgGenImage get lock => + const SvgGenImage('assets/icon/bold/lock.svg', size: Size(19.0, 19.0)); + + /// File path: assets/icon/bold/my-assistant.svg + SvgGenImage get myAssistant => const SvgGenImage( + 'assets/icon/bold/my-assistant.svg', + size: Size(22.0, 28.0), + ); + + /// File path: assets/icon/bold/pause.svg + SvgGenImage get pause => + const SvgGenImage('assets/icon/bold/pause.svg', size: Size(24.0, 24.0)); + + /// File path: assets/icon/bold/play.svg + SvgGenImage get play => + const SvgGenImage('assets/icon/bold/play.svg', size: Size(24.0, 24.0)); + + /// File path: assets/icon/bold/profile.svg + SvgGenImage get profile => + const SvgGenImage('assets/icon/bold/profile.svg', size: Size(80.0, 80.0)); + + /// File path: assets/icon/bold/send.svg + SvgGenImage get send => + const SvgGenImage('assets/icon/bold/send.svg', size: Size(19.0, 19.0)); + + /// File path: assets/icon/bold/setting.svg + SvgGenImage get setting => + const SvgGenImage('assets/icon/bold/setting.svg', size: Size(17.0, 17.0)); + + /// File path: assets/icon/bold/stop.svg + SvgGenImage get stop => + const SvgGenImage('assets/icon/bold/stop.svg', size: Size(21.0, 20.0)); + + /// File path: assets/icon/bold/verify.svg + SvgGenImage get verify => + const SvgGenImage('assets/icon/bold/verify.svg', size: Size(20.0, 21.0)); + + /// List of all assets + List get values => [ + archiveTick, + createAssistant, + dislike, + globalAssistant, + key, + like, + lock, + myAssistant, + pause, + play, + profile, + send, + setting, + stop, + verify, + ]; +} + +class $AssetsIconBulkGen { + const $AssetsIconBulkGen(); + + /// File path: assets/icon/bulk/assistant-dark.svg + SvgGenImage get assistantDark => const SvgGenImage( + 'assets/icon/bulk/assistant-dark.svg', + size: Size(22.0, 25.0), + ); + + /// File path: assets/icon/bulk/assistant.svg + SvgGenImage get assistant => const SvgGenImage( + 'assets/icon/bulk/assistant.svg', + size: Size(22.0, 25.0), + ); + + /// File path: assets/icon/bulk/audio.svg + SvgGenImage get audio => + const SvgGenImage('assets/icon/bulk/audio.svg', size: Size(49.0, 46.0)); + + /// File path: assets/icon/bulk/camera.svg + SvgGenImage get camera => + const SvgGenImage('assets/icon/bulk/camera.svg', size: Size(56.0, 48.0)); + + /// File path: assets/icon/bulk/characters-dark.svg + SvgGenImage get charactersDark => const SvgGenImage( + 'assets/icon/bulk/characters-dark.svg', + size: Size(25.0, 25.0), + ); + + /// File path: assets/icon/bulk/characters.svg + SvgGenImage get characters => const SvgGenImage( + 'assets/icon/bulk/characters.svg', + size: Size(25.0, 25.0), + ); + + /// File path: assets/icon/bulk/chat-dark.svg + SvgGenImage get chatDark => const SvgGenImage( + 'assets/icon/bulk/chat-dark.svg', + size: Size(24.0, 25.0), + ); + + /// File path: assets/icon/bulk/chat.svg + SvgGenImage get chat => + const SvgGenImage('assets/icon/bulk/chat.svg', size: Size(24.0, 25.0)); + + /// File path: assets/icon/bulk/coin.png + AssetGenImage get coin => const AssetGenImage( + 'assets/icon/bulk/coin.png', + size: Size(512.0, 512.0), + ); + + /// File path: assets/icon/bulk/gift.svg + SvgGenImage get gift => + const SvgGenImage('assets/icon/bulk/gift.svg', size: Size(56.0, 56.0)); + + /// File path: assets/icon/bulk/home-dark.svg + SvgGenImage get homeDark => const SvgGenImage( + 'assets/icon/bulk/home-dark.svg', + size: Size(25.0, 25.0), + ); + + /// File path: assets/icon/bulk/home.svg + SvgGenImage get home => + const SvgGenImage('assets/icon/bulk/home.svg', size: Size(25.0, 25.0)); + + /// File path: assets/icon/bulk/library-dark.svg + SvgGenImage get libraryDark => const SvgGenImage( + 'assets/icon/bulk/library-dark.svg', + size: Size(24.0, 25.0), + ); + + /// File path: assets/icon/bulk/library.svg + SvgGenImage get library => + const SvgGenImage('assets/icon/bulk/library.svg', size: Size(24.0, 25.0)); + + /// File path: assets/icon/bulk/media-dark.svg + SvgGenImage get mediaDark => const SvgGenImage( + 'assets/icon/bulk/media-dark.svg', + size: Size(25.0, 25.0), + ); + + /// File path: assets/icon/bulk/media.svg + SvgGenImage get media => + const SvgGenImage('assets/icon/bulk/media.svg', size: Size(25.0, 25.0)); + + /// File path: assets/icon/bulk/messages-dark.svg + SvgGenImage get messagesDark => const SvgGenImage( + 'assets/icon/bulk/messages-dark.svg', + size: Size(24.0, 25.0), + ); + + /// File path: assets/icon/bulk/messages.svg + SvgGenImage get messages => const SvgGenImage( + 'assets/icon/bulk/messages.svg', + size: Size(24.0, 25.0), + ); + + /// File path: assets/icon/bulk/news.svg + SvgGenImage get news => + const SvgGenImage('assets/icon/bulk/news.svg', size: Size(24.0, 25.0)); + + /// File path: assets/icon/bulk/setting-dark.svg + SvgGenImage get settingDark => const SvgGenImage( + 'assets/icon/bulk/setting-dark.svg', + size: Size(24.0, 25.0), + ); + + /// File path: assets/icon/bulk/setting.svg + SvgGenImage get setting => + const SvgGenImage('assets/icon/bulk/setting.svg', size: Size(24.0, 25.0)); + + /// File path: assets/icon/bulk/tool-box.svg + SvgGenImage get toolBox => const SvgGenImage( + 'assets/icon/bulk/tool-box.svg', + size: Size(24.0, 25.0), + ); + + /// File path: assets/icon/bulk/video.svg + SvgGenImage get video => + const SvgGenImage('assets/icon/bulk/video.svg', size: Size(59.0, 55.0)); + + /// File path: assets/icon/bulk/warning-2.svg + SvgGenImage get warning2 => const SvgGenImage( + 'assets/icon/bulk/warning-2.svg', + size: Size(24.0, 24.0), + ); + + /// List of all assets + List get values => [ + assistantDark, + assistant, + audio, + camera, + charactersDark, + characters, + chatDark, + chat, + coin, + gift, + homeDark, + home, + libraryDark, + library, + mediaDark, + media, + messagesDark, + messages, + news, + settingDark, + setting, + toolBox, + video, + warning2, + ]; +} + +class $AssetsIconGifGen { + const $AssetsIconGifGen(); + + /// File path: assets/icon/gif/alpha.gif + AssetGenImage get alpha => const AssetGenImage('assets/icon/gif/alpha.gif'); + + /// File path: assets/icon/gif/bell.gif + AssetGenImage get bell => + const AssetGenImage('assets/icon/gif/bell.gif', size: Size(400.0, 400.0)); + + /// File path: assets/icon/gif/beta.gif + AssetGenImage get beta => const AssetGenImage('assets/icon/gif/beta.gif'); + + /// File path: assets/icon/gif/chat-main.gif + AssetGenImage get chatMain => const AssetGenImage( + 'assets/icon/gif/chat-main.gif', + size: Size(640.0, 640.0), + ); + + /// File path: assets/icon/gif/clock.gif + AssetGenImage get clock => const AssetGenImage( + 'assets/icon/gif/clock.gif', + size: Size(400.0, 400.0), + ); + + /// File path: assets/icon/gif/coin.gif + AssetGenImage get coin => const AssetGenImage('assets/icon/gif/coin.gif'); + + /// File path: assets/icon/gif/delete.gif + AssetGenImage get delete => const AssetGenImage( + 'assets/icon/gif/delete.gif', + size: Size(640.0, 640.0), + ); + + /// File path: assets/icon/gif/empty-bookmarks.gif + AssetGenImage get emptyBookmarks => const AssetGenImage( + 'assets/icon/gif/empty-bookmarks.gif', + size: Size(150.0, 150.0), + ); + + /// File path: assets/icon/gif/exit.gif + AssetGenImage get exit => + const AssetGenImage('assets/icon/gif/exit.gif', size: Size(640.0, 640.0)); + + /// File path: assets/icon/gif/extras.gif + AssetGenImage get extras => const AssetGenImage('assets/icon/gif/extras.gif'); + + /// File path: assets/icon/gif/flash.gif + AssetGenImage get flash => + const AssetGenImage('assets/icon/gif/flash.gif', size: Size(52.0, 113.0)); + + /// File path: assets/icon/gif/gift.gif + AssetGenImage get gift => const AssetGenImage('assets/icon/gif/gift.gif'); + + /// File path: assets/icon/gif/heart.gif + AssetGenImage get heart => const AssetGenImage('assets/icon/gif/heart.gif'); + + /// File path: assets/icon/gif/instagram.gif + AssetGenImage get instagram => + const AssetGenImage('assets/icon/gif/instagram.gif'); + + /// File path: assets/icon/gif/medal.gif + AssetGenImage get medal => const AssetGenImage( + 'assets/icon/gif/medal.gif', + size: Size(400.0, 400.0), + ); + + /// File path: assets/icon/gif/one-coin.gif + AssetGenImage get oneCoin => + const AssetGenImage('assets/icon/gif/one-coin.gif'); + + /// File path: assets/icon/gif/organizational.gif + AssetGenImage get organizational => + const AssetGenImage('assets/icon/gif/organizational.gif'); + + /// File path: assets/icon/gif/wave_hand.gif + AssetGenImage get waveHand => const AssetGenImage( + 'assets/icon/gif/wave_hand.gif', + size: Size(400.0, 400.0), + ); + + /// File path: assets/icon/gif/write.gif + AssetGenImage get write => const AssetGenImage('assets/icon/gif/write.gif'); + + /// List of all assets + List get values => [ + alpha, + bell, + beta, + chatMain, + clock, + coin, + delete, + emptyBookmarks, + exit, + extras, + flash, + gift, + heart, + instagram, + medal, + oneCoin, + organizational, + waveHand, + write, + ]; +} + +class $AssetsIconLauncherIconsGen { + const $AssetsIconLauncherIconsGen(); + + /// File path: assets/icon/launcher_icons/houshan-icon-midround.png + AssetGenImage get houshanIconMidround => const AssetGenImage( + 'assets/icon/launcher_icons/houshan-icon-midround.png', + size: Size(800.0, 799.0), + ); + + /// File path: assets/icon/launcher_icons/houshan-icon-primary.png + AssetGenImage get houshanIconPrimary => const AssetGenImage( + 'assets/icon/launcher_icons/houshan-icon-primary.png', + size: Size(1181.0, 1181.0), + ); + + /// File path: assets/icon/launcher_icons/houshan-icon-rounded.ico + String get houshanIconRoundedIco => + 'assets/icon/launcher_icons/houshan-icon-rounded.ico'; + + /// File path: assets/icon/launcher_icons/houshan-icon-rounded.png + AssetGenImage get houshanIconRoundedPng => const AssetGenImage( + 'assets/icon/launcher_icons/houshan-icon-rounded.png', + size: Size(933.0, 933.0), + ); + + /// File path: assets/icon/launcher_icons/houshan-icon-whie.png + AssetGenImage get houshanIconWhie => const AssetGenImage( + 'assets/icon/launcher_icons/houshan-icon-whie.png', + size: Size(1181.0, 1181.0), + ); + + /// File path: assets/icon/launcher_icons/houshan-icon.png + AssetGenImage get houshanIcon => const AssetGenImage( + 'assets/icon/launcher_icons/houshan-icon.png', + size: Size(870.0, 870.0), + ); + + /// List of all assets + List get values => [ + houshanIconMidround, + houshanIconPrimary, + houshanIconRoundedIco, + houshanIconRoundedPng, + houshanIconWhie, + houshanIcon, + ]; +} + +class $AssetsIconNavbarsGen { + const $AssetsIconNavbarsGen(); + + /// Directory path: assets/icon/navbars/navigation + $AssetsIconNavbarsNavigationGen get navigation => + const $AssetsIconNavbarsNavigationGen(); + + /// Directory path: assets/icon/navbars/navigation-dark + $AssetsIconNavbarsNavigationDarkGen get navigationDark => + const $AssetsIconNavbarsNavigationDarkGen(); + + /// Directory path: assets/icon/navbars/navigation-light + $AssetsIconNavbarsNavigationLightGen get navigationLight => + const $AssetsIconNavbarsNavigationLightGen(); +} + +class $AssetsIconOutlineGen { + const $AssetsIconOutlineGen(); + + /// File path: assets/icon/outline/add.svg + SvgGenImage get add => + const SvgGenImage('assets/icon/outline/add.svg', size: Size(24.0, 24.0)); + + /// File path: assets/icon/outline/archive-tick.svg + SvgGenImage get archiveTick => const SvgGenImage( + 'assets/icon/outline/archive-tick.svg', + size: Size(18.0, 18.0), + ); + + /// File path: assets/icon/outline/arrow-flash-right.svg + SvgGenImage get arrowFlashRight => const SvgGenImage( + 'assets/icon/outline/arrow-flash-right.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/arrow-right.svg + SvgGenImage get arrowRight => const SvgGenImage( + 'assets/icon/outline/arrow-right.svg', + size: Size(17.0, 16.0), + ); + + /// File path: assets/icon/outline/assistant.svg + SvgGenImage get assistant => const SvgGenImage( + 'assets/icon/outline/assistant.svg', + size: Size(28.0, 28.0), + ); + + /// File path: assets/icon/outline/bitcoin-refresh.svg + SvgGenImage get bitcoinRefresh => const SvgGenImage( + 'assets/icon/outline/bitcoin-refresh.svg', + size: Size(16.0, 16.0), + ); + + /// File path: assets/icon/outline/brain.svg + SvgGenImage get brain => const SvgGenImage( + 'assets/icon/outline/brain.svg', + size: Size(19.0, 20.0), + ); + + /// File path: assets/icon/outline/brush.svg + SvgGenImage get brush => const SvgGenImage( + 'assets/icon/outline/brush.svg', + size: Size(20.0, 20.0), + ); + + /// File path: assets/icon/outline/calendar-edit.svg + SvgGenImage get calendarEdit => const SvgGenImage( + 'assets/icon/outline/calendar-edit.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/call.svg + SvgGenImage get call => + const SvgGenImage('assets/icon/outline/call.svg', size: Size(24.0, 24.0)); + + /// File path: assets/icon/outline/camera.svg + SvgGenImage get camera => const SvgGenImage( + 'assets/icon/outline/camera.svg', + size: Size(19.0, 19.0), + ); + + /// File path: assets/icon/outline/card-add.svg + SvgGenImage get cardAdd => const SvgGenImage( + 'assets/icon/outline/card-add.svg', + size: Size(19.0, 19.0), + ); + + /// File path: assets/icon/outline/card-pos.svg + SvgGenImage get cardPos => const SvgGenImage( + 'assets/icon/outline/card-pos.svg', + size: Size(20.0, 20.0), + ); + + /// File path: assets/icon/outline/characters.svg + SvgGenImage get characters => const SvgGenImage( + 'assets/icon/outline/characters.svg', + size: Size(25.0, 24.0), + ); + + /// File path: assets/icon/outline/chart.svg + SvgGenImage get chart => const SvgGenImage( + 'assets/icon/outline/chart.svg', + size: Size(20.0, 20.0), + ); + + /// File path: assets/icon/outline/chat.svg + SvgGenImage get chat => + const SvgGenImage('assets/icon/outline/chat.svg', size: Size(24.0, 24.0)); + + /// File path: assets/icon/outline/clock.svg + SvgGenImage get clock => const SvgGenImage( + 'assets/icon/outline/clock.svg', + size: Size(16.0, 17.0), + ); + + /// File path: assets/icon/outline/coin.svg + SvgGenImage get coin => + const SvgGenImage('assets/icon/outline/coin.svg', size: Size(25.0, 25.0)); + + /// File path: assets/icon/outline/copy.svg + SvgGenImage get copy => + const SvgGenImage('assets/icon/outline/copy.svg', size: Size(16.0, 16.0)); + + /// File path: assets/icon/outline/courthouse.svg + SvgGenImage get courthouse => const SvgGenImage( + 'assets/icon/outline/courthouse.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/crown.svg + SvgGenImage get crown => const SvgGenImage( + 'assets/icon/outline/crown.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/direct-inbox.svg + SvgGenImage get directInbox => const SvgGenImage( + 'assets/icon/outline/direct-inbox.svg', + size: Size(16.0, 16.0), + ); + + /// File path: assets/icon/outline/direct-send.svg + SvgGenImage get directSend => const SvgGenImage( + 'assets/icon/outline/direct-send.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/dislike.svg + SvgGenImage get dislike => const SvgGenImage( + 'assets/icon/outline/dislike.svg', + size: Size(16.0, 16.0), + ); + + /// File path: assets/icon/outline/document-copy.svg + SvgGenImage get documentCopy => const SvgGenImage( + 'assets/icon/outline/document-copy.svg', + size: Size(19.0, 18.0), + ); + + /// File path: assets/icon/outline/document-text.svg + SvgGenImage get documentText => const SvgGenImage( + 'assets/icon/outline/document-text.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/download.svg + SvgGenImage get download => const SvgGenImage( + 'assets/icon/outline/download.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/edit-2.svg + SvgGenImage get edit2 => const SvgGenImage( + 'assets/icon/outline/edit-2.svg', + size: Size(16.0, 16.0), + ); + + /// File path: assets/icon/outline/element-plus.svg + SvgGenImage get elementPlus => const SvgGenImage( + 'assets/icon/outline/element-plus.svg', + size: Size(19.0, 19.0), + ); + + /// File path: assets/icon/outline/emoji-happy.svg + SvgGenImage get emojiHappy => const SvgGenImage( + 'assets/icon/outline/emoji-happy.svg', + size: Size(18.0, 18.0), + ); + + /// File path: assets/icon/outline/empty-wallet-tick.svg + SvgGenImage get emptyWalletTick => const SvgGenImage( + 'assets/icon/outline/empty-wallet-tick.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/empty-wallet.svg + SvgGenImage get emptyWallet => const SvgGenImage( + 'assets/icon/outline/empty-wallet.svg', + size: Size(25.0, 24.0), + ); + + /// File path: assets/icon/outline/eraser.svg + SvgGenImage get eraser => const SvgGenImage( + 'assets/icon/outline/eraser.svg', + size: Size(16.0, 17.0), + ); + + /// File path: assets/icon/outline/filter.svg + SvgGenImage get filter => const SvgGenImage( + 'assets/icon/outline/filter.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/flag-2.svg + SvgGenImage get flag2 => const SvgGenImage( + 'assets/icon/outline/flag-2.svg', + size: Size(16.0, 16.0), + ); + + /// File path: assets/icon/outline/gallery-add.svg + SvgGenImage get galleryAdd => const SvgGenImage( + 'assets/icon/outline/gallery-add.svg', + size: Size(19.0, 19.0), + ); + + /// File path: assets/icon/outline/ghost.svg + SvgGenImage get ghost => const SvgGenImage( + 'assets/icon/outline/ghost.svg', + size: Size(18.0, 18.0), + ); + + /// File path: assets/icon/outline/gift.svg + SvgGenImage get gift => + const SvgGenImage('assets/icon/outline/gift.svg', size: Size(24.0, 24.0)); + + /// File path: assets/icon/outline/global-search.svg + SvgGenImage get globalSearch => const SvgGenImage( + 'assets/icon/outline/global-search.svg', + size: Size(18.0, 19.0), + ); + + /// File path: assets/icon/outline/hat.svg + SvgGenImage get hat => + const SvgGenImage('assets/icon/outline/hat.svg', size: Size(24.0, 24.0)); + + /// File path: assets/icon/outline/heart.svg + SvgGenImage get heart => const SvgGenImage( + 'assets/icon/outline/heart.svg', + size: Size(18.0, 18.0), + ); + + /// File path: assets/icon/outline/home.svg + SvgGenImage get home => + const SvgGenImage('assets/icon/outline/home.svg', size: Size(25.0, 24.0)); + + /// File path: assets/icon/outline/idea.svg + SvgGenImage get idea => + const SvgGenImage('assets/icon/outline/idea.svg', size: Size(18.0, 18.0)); + + /// File path: assets/icon/outline/import.svg + SvgGenImage get import => const SvgGenImage( + 'assets/icon/outline/import.svg', + size: Size(18.0, 19.0), + ); + + /// File path: assets/icon/outline/info-circle.svg + SvgGenImage get infoCircle => const SvgGenImage( + 'assets/icon/outline/info-circle.svg', + size: Size(16.0, 16.0), + ); + + /// File path: assets/icon/outline/lamp-charge.svg + SvgGenImage get lampCharge => const SvgGenImage( + 'assets/icon/outline/lamp-charge.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/library.svg + SvgGenImage get library => const SvgGenImage( + 'assets/icon/outline/library.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/like.svg + SvgGenImage get like => + const SvgGenImage('assets/icon/outline/like.svg', size: Size(16.0, 16.0)); + + /// File path: assets/icon/outline/lock.svg + SvgGenImage get lock => + const SvgGenImage('assets/icon/outline/lock.svg', size: Size(24.0, 24.0)); + + /// File path: assets/icon/outline/login.svg + SvgGenImage get login => const SvgGenImage( + 'assets/icon/outline/login.svg', + size: Size(21.0, 21.0), + ); + + /// File path: assets/icon/outline/mage_scan-user.svg + SvgGenImage get mageScanUser => const SvgGenImage( + 'assets/icon/outline/mage_scan-user.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/magicpen.svg + SvgGenImage get magicpen => const SvgGenImage( + 'assets/icon/outline/magicpen.svg', + size: Size(16.0, 16.0), + ); + + /// File path: assets/icon/outline/media.svg + SvgGenImage get media => const SvgGenImage( + 'assets/icon/outline/media.svg', + size: Size(25.0, 24.0), + ); + + /// File path: assets/icon/outline/message-question.svg + SvgGenImage get messageQuestion => const SvgGenImage( + 'assets/icon/outline/message-question.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/message-text.svg + SvgGenImage get messageText => const SvgGenImage( + 'assets/icon/outline/message-text.svg', + size: Size(16.0, 16.0), + ); + + /// File path: assets/icon/outline/messages.svg + SvgGenImage get messages => const SvgGenImage( + 'assets/icon/outline/messages.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/microphone-chat.svg + SvgGenImage get microphoneChat => const SvgGenImage( + 'assets/icon/outline/microphone-chat.svg', + size: Size(19.0, 19.0), + ); + + /// File path: assets/icon/outline/mobile.svg + SvgGenImage get mobile => const SvgGenImage( + 'assets/icon/outline/mobile.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/moon.svg + SvgGenImage get moon => + const SvgGenImage('assets/icon/outline/moon.svg', size: Size(18.0, 18.0)); + + /// File path: assets/icon/outline/more.svg + SvgGenImage get more => + const SvgGenImage('assets/icon/outline/more.svg', size: Size(20.0, 20.0)); + + /// File path: assets/icon/outline/musicnote.svg + SvgGenImage get musicnote => const SvgGenImage( + 'assets/icon/outline/musicnote.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/news.svg + SvgGenImage get news => + const SvgGenImage('assets/icon/outline/news.svg', size: Size(24.0, 24.0)); + + /// File path: assets/icon/outline/notification-bing.svg + SvgGenImage get notificationBing => const SvgGenImage( + 'assets/icon/outline/notification-bing.svg', + size: Size(20.0, 20.0), + ); + + /// File path: assets/icon/outline/pause.svg + SvgGenImage get pause => const SvgGenImage( + 'assets/icon/outline/pause.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/play.svg + SvgGenImage get play => + const SvgGenImage('assets/icon/outline/play.svg', size: Size(24.0, 24.0)); + + /// File path: assets/icon/outline/profile-tick.svg + SvgGenImage get profileTick => const SvgGenImage( + 'assets/icon/outline/profile-tick.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/profile-user-doual.svg + SvgGenImage get profileUserDoual => const SvgGenImage( + 'assets/icon/outline/profile-user-doual.svg', + size: Size(20.0, 20.0), + ); + + /// File path: assets/icon/outline/profile.svg + SvgGenImage get profile => const SvgGenImage( + 'assets/icon/outline/profile.svg', + size: Size(40.0, 40.0), + ); + + /// File path: assets/icon/outline/search-normal.svg + SvgGenImage get searchNormal => const SvgGenImage( + 'assets/icon/outline/search-normal.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/setting.svg + SvgGenImage get setting => const SvgGenImage( + 'assets/icon/outline/setting.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/share.svg + SvgGenImage get share => const SvgGenImage( + 'assets/icon/outline/share.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/shield-tick.svg + SvgGenImage get shieldTick => const SvgGenImage( + 'assets/icon/outline/shield-tick.svg', + size: Size(20.0, 20.0), + ); + + /// File path: assets/icon/outline/sms-tracking.svg + SvgGenImage get smsTracking => const SvgGenImage( + 'assets/icon/outline/sms-tracking.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/stop-circle.svg + SvgGenImage get stopCircle => const SvgGenImage( + 'assets/icon/outline/stop-circle.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/stop.svg + SvgGenImage get stop => + const SvgGenImage('assets/icon/outline/stop.svg', size: Size(24.0, 24.0)); + + /// File path: assets/icon/outline/sun.svg + SvgGenImage get sun => + const SvgGenImage('assets/icon/outline/sun.svg', size: Size(18.0, 18.0)); + + /// File path: assets/icon/outline/tick-circle.svg + SvgGenImage get tickCircle => const SvgGenImage( + 'assets/icon/outline/tick-circle.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/tick-square.svg + SvgGenImage get tickSquare => const SvgGenImage( + 'assets/icon/outline/tick-square.svg', + size: Size(18.0, 19.0), + ); + + /// File path: assets/icon/outline/timer.svg + SvgGenImage get timer => const SvgGenImage( + 'assets/icon/outline/timer.svg', + size: Size(48.0, 48.0), + ); + + /// File path: assets/icon/outline/tool-box.svg + SvgGenImage get toolBox => const SvgGenImage( + 'assets/icon/outline/tool-box.svg', + size: Size(24.0, 24.0), + ); + + /// File path: assets/icon/outline/translate.svg + SvgGenImage get translate => const SvgGenImage( + 'assets/icon/outline/translate.svg', + size: Size(16.0, 16.0), + ); + + /// File path: assets/icon/outline/trash.svg + SvgGenImage get trash => const SvgGenImage( + 'assets/icon/outline/trash.svg', + size: Size(16.0, 16.0), + ); + + /// File path: assets/icon/outline/verify.svg + SvgGenImage get verify => const SvgGenImage( + 'assets/icon/outline/verify.svg', + size: Size(18.0, 19.0), + ); + + /// File path: assets/icon/outline/video-play.svg + SvgGenImage get videoPlay => const SvgGenImage( + 'assets/icon/outline/video-play.svg', + size: Size(40.0, 40.0), + ); + + /// File path: assets/icon/outline/voice-cricle.svg + SvgGenImage get voiceCricle => const SvgGenImage( + 'assets/icon/outline/voice-cricle.svg', + size: Size(16.0, 16.0), + ); + + /// File path: assets/icon/outline/warning-2.svg + SvgGenImage get warning2 => const SvgGenImage( + 'assets/icon/outline/warning-2.svg', + size: Size(24.0, 25.0), + ); + + /// List of all assets + List get values => [ + add, + archiveTick, + arrowFlashRight, + arrowRight, + assistant, + bitcoinRefresh, + brain, + brush, + calendarEdit, + call, + camera, + cardAdd, + cardPos, + characters, + chart, + chat, + clock, + coin, + copy, + courthouse, + crown, + directInbox, + directSend, + dislike, + documentCopy, + documentText, + download, + edit2, + elementPlus, + emojiHappy, + emptyWalletTick, + emptyWallet, + eraser, + filter, + flag2, + galleryAdd, + ghost, + gift, + globalSearch, + hat, + heart, + home, + idea, + import, + infoCircle, + lampCharge, + library, + like, + lock, + login, + mageScanUser, + magicpen, + media, + messageQuestion, + messageText, + messages, + microphoneChat, + mobile, + moon, + more, + musicnote, + news, + notificationBing, + pause, + play, + profileTick, + profileUserDoual, + profile, + searchNormal, + setting, + share, + shieldTick, + smsTracking, + stopCircle, + stop, + sun, + tickCircle, + tickSquare, + timer, + toolBox, + translate, + trash, + verify, + videoPlay, + voiceCricle, + warning2, + ]; +} + +class $AssetsIconSigninGen { + const $AssetsIconSigninGen(); + + /// File path: assets/icon/signin/igoogle.svg + SvgGenImage get igoogle => const SvgGenImage( + 'assets/icon/signin/igoogle.svg', + size: Size(23.0, 22.0), + ); + + /// List of all assets + List get values => [igoogle]; +} + +class $AssetsIconSocialGen { + const $AssetsIconSocialGen(); + + /// Directory path: assets/icon/social/bold + $AssetsIconSocialBoldGen get bold => const $AssetsIconSocialBoldGen(); +} + +class $AssetsImageBoardingsGen { + const $AssetsImageBoardingsGen(); + + /// File path: assets/image/boardings/drei.png + AssetGenImage get drei => const AssetGenImage( + 'assets/image/boardings/drei.png', + size: Size(720.0, 796.0), + ); + + /// File path: assets/image/boardings/eins.png + AssetGenImage get eins => const AssetGenImage( + 'assets/image/boardings/eins.png', + size: Size(720.0, 796.0), + ); + + /// File path: assets/image/boardings/funf.png + AssetGenImage get funf => const AssetGenImage( + 'assets/image/boardings/funf.png', + size: Size(720.0, 796.0), + ); + + /// File path: assets/image/boardings/sechs.png + AssetGenImage get sechs => const AssetGenImage( + 'assets/image/boardings/sechs.png', + size: Size(720.0, 796.0), + ); + + /// File path: assets/image/boardings/vier.png + AssetGenImage get vier => const AssetGenImage( + 'assets/image/boardings/vier.png', + size: Size(720.0, 796.0), + ); + + /// File path: assets/image/boardings/zwei.png + AssetGenImage get zwei => const AssetGenImage( + 'assets/image/boardings/zwei.png', + size: Size(720.0, 796.0), + ); + + /// List of all assets + List get values => [drei, eins, funf, sechs, vier, zwei]; +} + +class $AssetsImageEmptyGen { + const $AssetsImageEmptyGen(); + + /// File path: assets/image/empty/amount.png + AssetGenImage get amount => const AssetGenImage( + 'assets/image/empty/amount.png', + size: Size(347.0, 433.0), + ); + + /// File path: assets/image/empty/assistant.png + AssetGenImage get assistant => const AssetGenImage( + 'assets/image/empty/assistant.png', + size: Size(347.0, 433.0), + ); + + /// File path: assets/image/empty/connection.png + AssetGenImage get connection => const AssetGenImage( + 'assets/image/empty/connection.png', + size: Size(347.0, 433.0), + ); + + /// File path: assets/image/empty/empty-text-underline.svg + SvgGenImage get emptyTextUnderline => const SvgGenImage( + 'assets/image/empty/empty-text-underline.svg', + size: Size(281.0, 7.0), + ); + + /// File path: assets/image/empty/inbox.png + AssetGenImage get inbox => const AssetGenImage( + 'assets/image/empty/inbox.png', + size: Size(347.0, 433.0), + ); + + /// File path: assets/image/empty/messages.png + AssetGenImage get messages => const AssetGenImage( + 'assets/image/empty/messages.png', + size: Size(347.0, 433.0), + ); + + /// File path: assets/image/empty/reverse-arrow.png + AssetGenImage get reverseArrow => const AssetGenImage( + 'assets/image/empty/reverse-arrow.png', + size: Size(57.0, 57.0), + ); + + /// File path: assets/image/empty/server.png + AssetGenImage get server => const AssetGenImage( + 'assets/image/empty/server.png', + size: Size(347.0, 433.0), + ); + + /// List of all assets + List get values => [ + amount, + assistant, + connection, + emptyTextUnderline, + inbox, + messages, + reverseArrow, + server, + ]; +} + +class $AssetsImageSplashGen { + const $AssetsImageSplashGen(); + + /// File path: assets/image/splash/splash-desk.png + AssetGenImage get splashDesk => const AssetGenImage( + 'assets/image/splash/splash-desk.png', + size: Size(1440.0, 787.0), + ); + + /// File path: assets/image/splash/splash.png + AssetGenImage get splash => const AssetGenImage( + 'assets/image/splash/splash.png', + size: Size(360.0, 800.0), + ); + + /// List of all assets + List get values => [splashDesk, splash]; +} + +class $AssetsIconNavbarsNavigationGen { + const $AssetsIconNavbarsNavigationGen(); + + /// File path: assets/icon/navbars/navigation/assistant.svg + SvgGenImage get assistant => const SvgGenImage( + 'assets/icon/navbars/navigation/assistant.svg', + size: Size(26.0, 28.0), + ); + + /// File path: assets/icon/navbars/navigation/characters.svg + SvgGenImage get characters => const SvgGenImage( + 'assets/icon/navbars/navigation/characters.svg', + size: Size(25.0, 24.0), + ); + + /// File path: assets/icon/navbars/navigation/home.svg + SvgGenImage get home => const SvgGenImage( + 'assets/icon/navbars/navigation/home.svg', + size: Size(25.0, 24.0), + ); + + /// File path: assets/icon/navbars/navigation/media.svg + SvgGenImage get media => const SvgGenImage( + 'assets/icon/navbars/navigation/media.svg', + size: Size(25.0, 24.0), + ); + + /// File path: assets/icon/navbars/navigation/setting.svg + SvgGenImage get setting => const SvgGenImage( + 'assets/icon/navbars/navigation/setting.svg', + size: Size(24.0, 24.0), + ); + + /// List of all assets + List get values => [assistant, characters, home, media, setting]; +} + +class $AssetsIconNavbarsNavigationDarkGen { + const $AssetsIconNavbarsNavigationDarkGen(); + + /// File path: assets/icon/navbars/navigation-dark/assistant.svg + SvgGenImage get assistant => const SvgGenImage( + 'assets/icon/navbars/navigation-dark/assistant.svg', + size: Size(22.0, 25.0), + ); + + /// File path: assets/icon/navbars/navigation-dark/characters.svg + SvgGenImage get characters => const SvgGenImage( + 'assets/icon/navbars/navigation-dark/characters.svg', + size: Size(25.0, 25.0), + ); + + /// File path: assets/icon/navbars/navigation-dark/home.svg + SvgGenImage get home => const SvgGenImage( + 'assets/icon/navbars/navigation-dark/home.svg', + size: Size(25.0, 25.0), + ); + + /// File path: assets/icon/navbars/navigation-dark/media.svg + SvgGenImage get media => const SvgGenImage( + 'assets/icon/navbars/navigation-dark/media.svg', + size: Size(25.0, 25.0), + ); + + /// File path: assets/icon/navbars/navigation-dark/setting.svg + SvgGenImage get setting => const SvgGenImage( + 'assets/icon/navbars/navigation-dark/setting.svg', + size: Size(24.0, 25.0), + ); + + /// List of all assets + List get values => [assistant, characters, home, media, setting]; +} + +class $AssetsIconNavbarsNavigationLightGen { + const $AssetsIconNavbarsNavigationLightGen(); + + /// File path: assets/icon/navbars/navigation-light/assistant.svg + SvgGenImage get assistant => const SvgGenImage( + 'assets/icon/navbars/navigation-light/assistant.svg', + size: Size(22.0, 25.0), + ); + + /// File path: assets/icon/navbars/navigation-light/characters.svg + SvgGenImage get characters => const SvgGenImage( + 'assets/icon/navbars/navigation-light/characters.svg', + size: Size(25.0, 25.0), + ); + + /// File path: assets/icon/navbars/navigation-light/home.svg + SvgGenImage get home => const SvgGenImage( + 'assets/icon/navbars/navigation-light/home.svg', + size: Size(25.0, 25.0), + ); + + /// File path: assets/icon/navbars/navigation-light/media.svg + SvgGenImage get media => const SvgGenImage( + 'assets/icon/navbars/navigation-light/media.svg', + size: Size(25.0, 25.0), + ); + + /// File path: assets/icon/navbars/navigation-light/setting.svg + SvgGenImage get setting => const SvgGenImage( + 'assets/icon/navbars/navigation-light/setting.svg', + size: Size(24.0, 25.0), + ); + + /// List of all assets + List get values => [assistant, characters, home, media, setting]; +} + +class $AssetsIconSocialBoldGen { + const $AssetsIconSocialBoldGen(); + + /// File path: assets/icon/social/bold/instagram.svg + SvgGenImage get instagram => const SvgGenImage( + 'assets/icon/social/bold/instagram.svg', + size: Size(25.0, 24.0), + ); + + /// File path: assets/icon/social/bold/linkdin.svg + SvgGenImage get linkdin => const SvgGenImage( + 'assets/icon/social/bold/linkdin.svg', + size: Size(25.0, 24.0), + ); + + /// File path: assets/icon/social/bold/site.svg + SvgGenImage get site => const SvgGenImage( + 'assets/icon/social/bold/site.svg', + size: Size(25.0, 24.0), + ); + + /// File path: assets/icon/social/bold/telegram.svg + SvgGenImage get telegram => const SvgGenImage( + 'assets/icon/social/bold/telegram.svg', + size: Size(25.0, 24.0), + ); + + /// File path: assets/icon/social/bold/twitter.svg + SvgGenImage get twitter => const SvgGenImage( + 'assets/icon/social/bold/twitter.svg', + size: Size(22.0, 24.0), + ); + + /// File path: assets/icon/social/bold/youtube.svg + SvgGenImage get youtube => const SvgGenImage( + 'assets/icon/social/bold/youtube.svg', + size: Size(25.0, 24.0), + ); + + /// List of all assets + List get values => [ + instagram, + linkdin, + site, + telegram, + twitter, + youtube, + ]; +} + +class Assets { + const Assets._(); + + static const $AssetsIconGen icon = $AssetsIconGen(); + static const $AssetsImageGen image = $AssetsImageGen(); +} + +class AssetGenImage { + const AssetGenImage(this._assetName, {this.size, this.flavors = const {}}); + + final String _assetName; + + final Size? size; + final Set flavors; + + Image image({ + Key? key, + AssetBundle? bundle, + ImageFrameBuilder? frameBuilder, + ImageErrorWidgetBuilder? errorBuilder, + String? semanticLabel, + bool excludeFromSemantics = false, + double? scale, + double? width, + double? height, + Color? color, + Animation? opacity, + BlendMode? colorBlendMode, + BoxFit? fit, + AlignmentGeometry alignment = Alignment.center, + ImageRepeat repeat = ImageRepeat.noRepeat, + Rect? centerSlice, + bool matchTextDirection = false, + bool gaplessPlayback = true, + bool isAntiAlias = false, + String? package, + FilterQuality filterQuality = FilterQuality.medium, + int? cacheWidth, + int? cacheHeight, + }) { + return Image.asset( + _assetName, + key: key, + bundle: bundle, + frameBuilder: frameBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + scale: scale, + width: width, + height: height, + color: color, + opacity: opacity, + colorBlendMode: colorBlendMode, + fit: fit, + alignment: alignment, + repeat: repeat, + centerSlice: centerSlice, + matchTextDirection: matchTextDirection, + gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, + package: package, + filterQuality: filterQuality, + cacheWidth: cacheWidth, + cacheHeight: cacheHeight, + ); + } + + ImageProvider provider({AssetBundle? bundle, String? package}) { + return AssetImage(_assetName, bundle: bundle, package: package); + } + + String get path => _assetName; + + String get keyName => _assetName; +} + +class SvgGenImage { + const SvgGenImage(this._assetName, {this.size, this.flavors = const {}}) + : _isVecFormat = false; + + const SvgGenImage.vec(this._assetName, {this.size, this.flavors = const {}}) + : _isVecFormat = true; + + final String _assetName; + final Size? size; + final Set flavors; + final bool _isVecFormat; + + _svg.SvgPicture svg({ + Key? key, + bool matchTextDirection = false, + AssetBundle? bundle, + String? package, + double? width, + double? height, + BoxFit fit = BoxFit.contain, + AlignmentGeometry alignment = Alignment.center, + bool allowDrawingOutsideViewBox = false, + WidgetBuilder? placeholderBuilder, + String? semanticsLabel, + bool excludeFromSemantics = false, + _svg.SvgTheme? theme, + ColorFilter? colorFilter, + Clip clipBehavior = Clip.hardEdge, + @deprecated Color? color, + @deprecated BlendMode colorBlendMode = BlendMode.srcIn, + @deprecated bool cacheColorFilter = false, + }) { + final _svg.BytesLoader loader; + if (_isVecFormat) { + loader = _vg.AssetBytesLoader( + _assetName, + assetBundle: bundle, + packageName: package, + ); + } else { + loader = _svg.SvgAssetLoader( + _assetName, + assetBundle: bundle, + packageName: package, + theme: theme, + ); + } + return _svg.SvgPicture( + loader, + key: key, + matchTextDirection: matchTextDirection, + width: width, + height: height, + fit: fit, + alignment: alignment, + allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, + placeholderBuilder: placeholderBuilder, + semanticsLabel: semanticsLabel, + excludeFromSemantics: excludeFromSemantics, + colorFilter: + colorFilter ?? + (color == null ? null : ColorFilter.mode(color, colorBlendMode)), + clipBehavior: clipBehavior, + cacheColorFilter: cacheColorFilter, + ); + } + + String get path => _assetName; + + String get keyName => _assetName; +} diff --git a/lib/core/gen/fonts.gen.dart b/lib/core/gen/fonts.gen.dart new file mode 100644 index 0000000..2b7650e --- /dev/null +++ b/lib/core/gen/fonts.gen.dart @@ -0,0 +1,18 @@ +/// GENERATED CODE - DO NOT MODIFY BY HAND +/// ***************************************************** +/// FlutterGen +/// ***************************************************** + +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use + +class FontFamily { + FontFamily._(); + + /// Font family: CustomIcons + static const String customIcons = 'CustomIcons'; + + /// Font family: Dana + static const String dana = 'Dana'; +} diff --git a/lib/core/gen/my_flutter_app_icons.dart b/lib/core/gen/my_flutter_app_icons.dart new file mode 100644 index 0000000..1ab3453 --- /dev/null +++ b/lib/core/gen/my_flutter_app_icons.dart @@ -0,0 +1,27 @@ +/// Flutter icons MyFlutterApp +/// Copyright (C) 2025 by original authors @ fluttericon.com, fontello.com +/// This font was generated by FlutterIcon.com, which is derived from Fontello. +/// +/// To use this font, place it in your fonts/ directory and include the +/// following in your pubspec.yaml +/// +/// flutter: +/// fonts: +/// - family: MyFlutterApp +/// fonts: +/// - asset: fonts/MyFlutterApp.ttf +/// +/// +/// +library; +import 'package:flutter/widgets.dart'; + +class CustomIcons { + CustomIcons._(); + + static const _kFontFam = 'CustomIcons'; + static const String? _kFontPkg = null; + + static const IconData ghost = + IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); +} diff --git a/lib/core/routes/route_generator.dart b/lib/core/routes/route_generator.dart new file mode 100644 index 0000000..9a3c827 --- /dev/null +++ b/lib/core/routes/route_generator.dart @@ -0,0 +1,579 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/utils/date_time.dart'; +import 'package:hoshan/data/model/ai/messages_model.dart'; +import 'package:hoshan/data/model/ai/send_message_model.dart'; +import 'package:hoshan/data/model/assistant_personal_info_model.dart'; +import 'package:hoshan/data/model/chat_args.dart'; +import 'package:hoshan/data/model/home_args.dart'; +import 'package:hoshan/data/model/purchase_args.dart'; +import 'package:hoshan/data/model/tools_categories_model.dart'; +import 'package:hoshan/data/storage/shared_preferences_helper.dart'; +import 'package:hoshan/main.dart'; +import 'package:hoshan/ui/screens/assistant/assistant_page.dart'; +import 'package:hoshan/ui/screens/assistant/bloc/assistant_info_bloc.dart'; +import 'package:hoshan/ui/screens/assistant/bloc/same_assistants_bloc.dart'; +import 'package:hoshan/ui/screens/assistant/cubit/assistant_comments_cubit.dart'; +import 'package:hoshan/ui/screens/auth/auth_page.dart'; +import 'package:hoshan/ui/screens/auth/cubit/auth_screens_cubit.dart'; +import 'package:hoshan/ui/screens/auth/gift/gift_credit_screen.dart'; +import 'package:hoshan/ui/screens/chat/bloc/messages_bloc.dart'; +import 'package:hoshan/ui/screens/chat/bloc/related_questions_bloc.dart'; +import 'package:hoshan/ui/screens/chat/chat_page.dart'; +import 'package:hoshan/ui/screens/chat/cubit/receive_message_cubit.dart'; +import 'package:hoshan/ui/screens/cmp/cmp_page.dart'; +import 'package:hoshan/ui/screens/faq/faq_page.dart'; +import 'package:hoshan/ui/screens/gmedia/cubit/media_g_response_cubit.dart'; +import 'package:hoshan/ui/screens/gmedia/cubit/single_media_cubit.dart'; +import 'package:hoshan/ui/screens/gmedia/generators/generate_audio_page.dart'; +import 'package:hoshan/ui/screens/gmedia/generators/generate_photo_page.dart'; +import 'package:hoshan/ui/screens/gmedia/generators/generate_video_page.dart'; +import 'package:hoshan/ui/screens/gmedia/chats/photo_chat_page.dart'; +import 'package:hoshan/ui/screens/main/assistant/bloc/create_assistant_bloc.dart'; +import 'package:hoshan/ui/screens/main/assistant/create_assistant_page.dart'; +import 'package:hoshan/ui/screens/main/assistant/cubit/delete_assistant_cubit.dart'; +import 'package:hoshan/ui/screens/main/home/bloc/courses_bloc.dart'; +import 'package:hoshan/ui/screens/main/home/cubit/banners_cubit.dart'; +import 'package:hoshan/ui/screens/main/home/cubit/best_assistants_cubit.dart'; +import 'package:hoshan/ui/screens/main/forum/cubit/comments_cubit.dart'; +import 'package:hoshan/ui/screens/main/home_page.dart'; +import 'package:hoshan/ui/screens/purchase_page/bloc/plans_bloc.dart'; +import 'package:hoshan/ui/screens/purchase_page/purchase_page.dart'; +import 'package:hoshan/ui/screens/setting/bloc/report_of_use_bloc.dart'; +import 'package:hoshan/ui/screens/setting/cubit/check_username_cubit.dart'; +import 'package:hoshan/ui/screens/setting/cubit/report_pi_coin_cubit.dart'; +import 'package:hoshan/ui/screens/setting/edit_profile_page.dart'; +import 'package:hoshan/ui/screens/setting/income_page.dart'; +import 'package:hoshan/ui/screens/setting/my_account_page.dart'; +import 'package:hoshan/ui/screens/setting/setting_page.dart'; +import 'package:hoshan/ui/screens/setting/utilization_report_page.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/screens/splash/splash_page.dart'; +import 'package:hoshan/ui/screens/ticket/bloc/send_ticket_bloc.dart'; +import 'package:hoshan/ui/screens/ticket/ticket_page.dart'; +import 'package:hoshan/ui/screens/tools/single_tool_page.dart'; +import 'package:hoshan/ui/screens/tools/tools_page.dart'; + +class Routes { + static const String main = '/'; + static const String onBoarding = '/on-boarding'; + static const String auth = '/auth'; + + static const String giftCredit = '/gift-credit'; + + static const String home = '/home'; + static const String setting = '$home/setting'; + static const String cmp = '$home/cmp'; + static const String generatPhoto = '$home/generator-photo'; + static const String generatAudio = '$home/generator-audio'; + static const String generatVideo = '$home/generator-video'; + static const String editProfile = '$home/edit-profile'; + static const String utilizationReport = '$home/utilization-report'; + static const String myAccount = '$home/my-account'; + static const String otherProducts = '$home/other-products'; + static const String chat = '$home/chat'; + static const String ticket = '$home/ticket'; + static const String income = '$home/income'; + static const String purchase = '$home/purchase'; + static const String createAssistant = '$home/create-assistant'; + static const String returnToApp = '/return-to-app'; + static const String assistant = '$home/assistant'; + static const String chatFromAssistant = '$assistant/chat'; + static const String faq = '$home/faq'; + static const String tools = '$home/tools'; + static const String singleTool = '$home/single-tool'; + static const String chatFromSingleTool = '$singleTool/chat'; + + static const String photoToPhoto = '$generatPhoto/ptp'; + static const String textToAudio = '$generatAudio/tta'; + + static bool showOnBoarding = true; + static Categories? categories; + static ChatArgs? chatArgs; + static int? lastPhotoId; + + static GoRoute _chatRoute(String path) => GoRoute( + path: path, + builder: (context, state) { + try { + if (state.extra != null) { + chatArgs = state.extra as ChatArgs; + } + } catch (e) { + if (kDebugMode) { + print('Error get Args: $e'); + } + } + if (chatArgs != null) { + final args = chatArgs!; + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) { + MessagesBloc messagesBloc = MessagesBloc(); + if (args.chatId != null) { + messagesBloc.add(GetallMessages(chatId: args.chatId!)); + } + if (args.messages != null) { + List cs = []; + for (var content in args.messages!.content!) { + if (content.type != 'text') { + cs.add(content); + } + } + messagesBloc.add(AddMessage( + message: Messages( + query: args.messages!.query, + file: args.messages!.file, + content: [ + ...cs, + Content(text: args.messages!.query, type: 'text'), + ], + role: 'human', + createdAt: DateTimeUtils.getNow().toIso8601String(), + id: 'hero', + ))); + } + + return messagesBloc; + }, + ), + BlocProvider( + create: (context) { + ReceiveMessageCubit receiveMessageCubit = + ReceiveMessageCubit(); + if (args.messages != null) { + receiveMessageCubit.execute( + request: SendMessageModel( + botId: args.bot.id, + file: args.messages!.file, + messageId: 'hero', + query: args.messages!.query!, + tool: args.bot.tool, + ), + ); + } + return receiveMessageCubit; + }, + ), + BlocProvider( + create: (context) => RelatedQuestionsBloc(), + ), + ], + child: ChatPage( + chatArgs: args, + ), + ); + } else { + context.pop(); + return const SizedBox.shrink(); + } + }, + ); + + static final GoRouter routeGenerator = GoRouter( + initialLocation: main, + navigatorKey: navigatorKey, + redirect: (context, state) async { + final uri = state.uri; + if (uri.scheme == 'houshan' && uri.host == 'auth') { + final token = uri.queryParameters['token']; + if (token != null && token.isNotEmpty) { + if (kDebugMode) { + print('Deep link token received: ${token.substring(0, 20)}...'); + } + await AuthTokenStorage.setToken(token); + await OnBoardingStorage + .setAsSeen(); + showOnBoarding = false; + if (kDebugMode) { + print('Token saved. Redirecting to main...'); + } + + return main; + } + } + + if (AuthTokenStorage.getToken().isEmpty) { + try { + showOnBoarding = state.extra as bool; + } catch (e) { + showOnBoarding = !OnBoardingStorage.hasSeen(); + if (kDebugMode) { + print('Error get Args: $e'); + print('Onboarding seen status: ${OnBoardingStorage.hasSeen()}'); + } + } + return auth; + } + + if (state.matchedLocation == auth) { + if (context.read().state is UserInfoFail) return null; + if (AuthTokenStorage.getToken().isNotEmpty || + (UserInfoCubit.userInfoModel.login != null && + !(UserInfoCubit.userInfoModel.login!))) { + return main; + } + } + if (state.matchedLocation == home && kIsWeb) { + if (UserInfoCubit.userInfoModel.login == null) { + return main; + } else { + if (!(UserInfoCubit.userInfoModel.login!)) { + return main; + } + } + } + + return null; + }, + routes: [ + GoRoute( + path: main, + builder: (context, state) { + return const SplashPage(); + }, + ), + GoRoute( + path: giftCredit, + builder: (context, state) { + return const GiftCreditScreen(); + }, + ), + GoRoute( + path: auth, + builder: (context, state) { + try { + showOnBoarding = state.extra as bool; + } catch (e) { + // Check if onboarding was already seen + showOnBoarding = !OnBoardingStorage.hasSeen(); + if (kDebugMode) { + print('Error get Args: $e'); + } + } + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => AuthScreensCubit(), + ) + ], + child: AuthPage( + show: showOnBoarding, + )); + }, + ), + GoRoute( + path: home, + builder: (context, state) { + HomeArgs? args; + + try { + args = state.extra as HomeArgs?; + } catch (e) { + if (kDebugMode) { + print('erros in purchase show: $e'); + } + } + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => BannersCubit()..getBanners(), + ), + BlocProvider( + create: (context) => CoursesBloc(), + ), + BlocProvider( + create: (context) => CommentsCubit()..loadComments(cId: 1), + ), + BlocProvider( + create: (context) => BestAssistantsCubit()..getAssistants(), + ), + ], + child: HomePage( + args: args, + ), + ); + }, + routes: [ + GoRoute( + path: setting.substring(home.length + 1), + builder: (context, state) { + return const SettingPage(); + }, + ), + GoRoute( + path: cmp.substring(home.length + 1), + builder: (context, state) { + return const CmpPage(); + }, + ), + GoRoute( + path: generatPhoto.substring(home.length + 1), + builder: (context, state) { + try { + final id = state.extra as int; + lastPhotoId = id; + } catch (e) { + lastPhotoId = null; + if (kDebugMode) { + print(e); + } + final id = state.uri.queryParameters['id']; + if (id != null) { + lastPhotoId = int.parse(id); + } + } + if (lastPhotoId != null) { + context.read().getMediaById(lastPhotoId!); + } + return const GeneratePhotoPage(); + }, + routes: [ + GoRoute( + path: 'ptp', + builder: (context, state) { + try { + if (state.extra != null) { + chatArgs = state.extra as ChatArgs; + } + } catch (e) { + if (kDebugMode) { + print('Error get Args: $e'); + } + } + if (chatArgs == null) { + context.pop(); + return const SizedBox(); + } + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) { + final messagesBloc = MessagesBloc(); + if (chatArgs!.chatId != null) { + messagesBloc.add(GetallMessages( + chatId: chatArgs!.chatId!)); + } + return messagesBloc; + }, + ), + BlocProvider( + create: (context) { + return MediaGResponseCubit(); + }, + ), + ], + child: PhotoChatPage( + chatArgs: chatArgs!, + ), + ); + }) + ]), + GoRoute( + path: generatVideo.substring(home.length + 1), + builder: (context, state) { + int? id; + try { + final i = state.extra as int; + id = i; + } catch (e) { + lastPhotoId = null; + if (kDebugMode) { + print(e); + } + final i = state.uri.queryParameters['id']; + if (i != null) { + id = int.parse(i); + } + } + if (id == null) { + context.pop(); + return SizedBox(); + } + return GenerateVideoPage(id: id); + }, + ), + GoRoute( + path: generatAudio.substring(home.length + 1), + builder: (context, state) { + int? id; + try { + final i = state.extra as int; + id = i; + } catch (e) { + lastPhotoId = null; + if (kDebugMode) { + print(e); + } + final i = state.uri.queryParameters['id']; + if (i != null) { + id = int.parse(i); + } + } + if (id == null) { + context.pop(); + return SizedBox(); + } + return GenerateAudioPage( + id: id, + ); + }, + routes: const []), + GoRoute( + path: editProfile.substring(home.length + 1), + builder: (context, state) { + return BlocProvider( + create: (context) => CheckUsernameCubit(), + child: const EditProfilePage(), + ); + }, + ), + GoRoute( + path: utilizationReport.substring(home.length + 1), + builder: (context, state) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => ReportPiCoinCubit()..getReport(), + ), + BlocProvider( + create: (context) => + ReportOfUseBloc()..add(const GetReport()), + ) + ], + child: const UtilizationReportPage(), + ); + }, + ), + GoRoute( + path: myAccount.substring(home.length + 1), + builder: (context, state) { + return const MyAccountPage(); + }, + ), + _chatRoute(chat.substring(home.length + 1)), + GoRoute( + path: ticket.substring(home.length + 1), + builder: (context, state) { + return BlocProvider( + create: (context) => SendTicketBloc()..add(GetTickets()), + child: const TicketPage(), + ); + }, + ), + GoRoute( + path: income.substring(home.length + 1), + builder: (context, state) { + return const IncomePage(); + }, + ), + GoRoute( + path: purchase.substring(home.length + 1), + builder: (context, state) { + PurchaseArgs? args; + if (state.uri.hasQuery) { + try { + final success = + state.uri.queryParameters['status'] == 'success'; + final message = state.uri.queryParameters['msg'] ?? ''; + final credit = int.tryParse( + state.uri.queryParameters['credit'].toString()) ?? + 0; + args = PurchaseArgs( + message: message, success: success, credit: credit); + } catch (e) { + if (kDebugMode) { + print('erros in purchase show: $e'); + } + } + } + + return BlocProvider( + create: (context) => PlansBloc()..add(GetAllPlans()), + child: PurchasePage( + args: args, + ), + ); + }, + ), + GoRoute( + path: createAssistant.substring(home.length + 1), + builder: (context, state) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => CheckUsernameCubit()), + BlocProvider( + create: (context) => CreateAssistantBloc(), + ), + BlocProvider( + create: (context) => DeleteAssistantCubit(), + ), + ], + child: CreateAssistantPage( + info: state.extra as AssistantPersonalInfo?, + ), + ); + }, + ), + GoRoute( + path: assistant.substring(home.length + 1), + builder: (context, state) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => SameAssistantsBloc()), + BlocProvider( + create: (context) => AssistantCommentsCubit()), + BlocProvider( + create: (context) => AssistantInfoBloc() + ..add(Getinfo(id: state.extra as int)), + ) + ], + child: const AssistantPage(), + ); + }, + routes: [_chatRoute(chat.substring(home.length + 1))]), + GoRoute( + path: faq.substring(home.length + 1), + builder: (context, state) { + return const FaqPage(); + }, + ), + GoRoute( + path: tools.substring(home.length + 1), + builder: (context, state) { + return const ToolsPage(); + }, + ), + GoRoute( + path: singleTool.substring(home.length + 1), + builder: (context, state) { + try { + if (state.extra != null) { + categories = state.extra as Categories; + } + } catch (e) { + if (kDebugMode) { + print('Error get Args: $e'); + } + } + if (categories == null) { + context.pop(); + return const SizedBox.shrink(); + } else { + return SingleToolPage( + cat: categories!, + ); + } + }, + routes: [_chatRoute(chat.substring(home.length + 1))]) + ]), + ], + ); +} diff --git a/lib/core/services/ad/adivery_service.dart b/lib/core/services/ad/adivery_service.dart new file mode 100644 index 0000000..da2c553 --- /dev/null +++ b/lib/core/services/ad/adivery_service.dart @@ -0,0 +1,35 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:pretty_dio_logger/pretty_dio_logger.dart'; + +class AdiveryService { + final String appId = 'ecc70180-4f9e-4ee9-b017-87cc5d34f0b2'; + final String placementId = 'd67314c0-7fe8-4fd1-b409-7088cfb50485'; + final String placementIdTest = 'd3d19c2a-142c-4551-92f1-1d2c38aea3ec'; + static const _canLog = kDebugMode && !kIsWeb; + + static final Dio _dio = Dio(BaseOptions( + baseUrl: 'https://fetch.adivery.com/api/v1', + connectTimeout: const Duration(minutes: 1), + responseType: ResponseType.json, + )) + ..interceptors.add(PrettyDioLogger(enabled: _canLog)); + + void initialize(BuildContext context) async { + await _dio.post('/installation', data: { + "device": { + "os": "Android", + "screen_dpi": 0, + "screen_width": MediaQuery.of(context).size.width.round(), + "screen_height": MediaQuery.of(context).size.height.round(), + "api_level": 16 + }, + "package": 'com.adivery.flutter', + "version_code": 1, + "app_id": appId, + "update_time": 0, + "install_time": 0, + }); + } +} diff --git a/lib/core/services/ad/cubit/on_show_add_cubit.dart b/lib/core/services/ad/cubit/on_show_add_cubit.dart new file mode 100644 index 0000000..b9b94db --- /dev/null +++ b/lib/core/services/ad/cubit/on_show_add_cubit.dart @@ -0,0 +1,28 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'on_show_add_state.dart'; + +class OnShowAddCubit extends Cubit { + OnShowAddCubit() : super(OnHideAdd()); + + void showAdd() { + emit(OnShowAdd()); + } + + void hideAdd() { + emit(OnHideAdd()); + } + + void toggleAdd() { + if (state is OnShowAdd) { + emit(OnHideAdd()); + } else { + emit(OnShowAdd()); + } + } + + void reset() { + emit(OnHideAdd()); + } +} diff --git a/lib/core/services/ad/cubit/on_show_add_state.dart b/lib/core/services/ad/cubit/on_show_add_state.dart new file mode 100644 index 0000000..7898fa3 --- /dev/null +++ b/lib/core/services/ad/cubit/on_show_add_state.dart @@ -0,0 +1,12 @@ +part of 'on_show_add_cubit.dart'; + +sealed class OnShowAddState extends Equatable { + const OnShowAddState(); + + @override + List get props => []; +} + +final class OnShowAdd extends OnShowAddState {} + +final class OnHideAdd extends OnShowAddState {} diff --git a/lib/core/services/ad/tapsell_service.dart b/lib/core/services/ad/tapsell_service.dart new file mode 100644 index 0000000..ebf0ff0 --- /dev/null +++ b/lib/core/services/ad/tapsell_service.dart @@ -0,0 +1,130 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/core/services/ad/cubit/on_show_add_cubit.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/ui/screens/setting/cubit/ad_remaining_cubit.dart'; +import 'package:tapsell_plus/tapsell_plus.dart'; + +class TapsellService { + static const appId = + 'ameghpchlcbcibfnktqaaigmfnpogqrtlrkdblirmgmipjfdjtdhppfhiipdieicortsrg'; + static const rewardedVideoZone = '67ff817a63943b49f700b6a9'; + static const standardZone = '67ff819f63943b49f700b6aa'; + // static const interstitialZone = '5cfaa942e8d17f0001ffb292'; + + // static const zoneIds = { + // "Tapsell": { + // "REWARDED": '5cfaa802e8d17f0001ffb28e', + // "INTERSTITIAL": '5cfaa942e8d17f0001ffb292', + // "NATIVE": '5cfaa9deaede570001d5553a', + // "STANDARD": '5cfaaa30e8d17f0001ffb294', + // }, + // "Google AdMob": { + // "REWARDED": '5cfaa8aee8d17f0001ffb28f', + // "INTERSTITIAL": '5cfaa9b0e8d17f0001ffb293', + // "NATIVE": '5d123c9968287d00019e1a94', + // "STANDARD": '5cfaaa4ae8d17f0001ffb295', + // }, + // "AdColony": { + // "REWARDED": '5d3362766de9f600013662d5', + // "INTERSTITIAL": '5d336289e985d50001427acf', + // "STANDARD": '60bf4ef0d40d970001693745', + // }, + // "AppLovin": { + // "REWARDED": '5d3eb48c3aef7a0001406f84', + // "INTERSTITIAL": '5d3eb4fa3aef7a0001406f85', + // "STANDARD": '5d3eb5337a9b060001892441', + // }, + // "Chart boost": { + // "REWARDED": '5cfaa8cee8d17f0001ffb290', + // "INTERSTITIAL": '60c5b303d756bf0001891f1c' + // }, + // "Unity Ads": { + // "REWARDED": '5cfaa8eae8d17f0001ffb291', + // "INTERSTITIAL": '608d1c1c2d8e7e0001348111', + // "STANDARD": '608d20a7fb661b000190bfe4', + // } + // }; + + static Future initialize() async { + try { + TapsellPlus.instance.initialize(appId); + TapsellPlus.instance.setGDPRConsent(true); + } catch (e) { + if (kDebugMode) { + print('Error initializing Tapsell: $e'); + } + } + } + + // static Future getResponseId(String type) async { + // final responseId = await TapsellPlus.instance + // .requestRewardedVideoAd(zoneIds['Tapsell']![type]!); + // return responseId; + // } + + static Future showAdRewarded(BuildContext context) async { + final responseId = + await TapsellPlus.instance.requestRewardedVideoAd(rewardedVideoZone); + + await TapsellPlus.instance.showRewardedVideoAd( + responseId, + onOpened: (map) { + // Ad opened + }, + onError: (map) {}, + onClosed: (map) { + // Ad closed + }, + onRewarded: (map) async { + try { + await DioService().sendRequest().post('/advertisement/claim', + data: {"ad_type": "rewarded_video"}); + } catch (e) { + if (kDebugMode) { + print('Error claiming ad: $e'); + } + } + context.read().getRemainingAd(); + }, + ); + } + + Future showAdInterstitial() async { + final responseId = + await TapsellPlus.instance.requestRewardedVideoAd(rewardedVideoZone); + TapsellPlus.instance.showInterstitialAd(responseId, onOpened: (map) { + // Ad opened + }, onError: (map) { + // Ad had error - map contains `error_message` + }, onClosed: (map) { + // Ad closed + }); + } + + static Future showAdBanner(BuildContext context) async { + // Banner ads are disabled + await TapsellPlus.instance.hideStandardBanner(); + // if (context.read().state is OnShowAdd) { + // await TapsellPlus.instance.hideStandardBanner(); + // } else { + // await TapsellPlus.instance.requestStandardBannerAd( + // standardZone, TapsellPlusBannerType.BANNER_320x50, + // onResponse: (map) async { + // await TapsellPlus.instance.showStandardBannerAd( + // map['response_id']!, + // TapsellPlusHorizontalGravity.BOTTOM, + // TapsellPlusVerticalGravity.CENTER); + + // // SAVE the responseId + // }, onError: (map) { + // // Error when requesting for an ad + // }); + // } + + // context.read().toggleAdd(); + } +} diff --git a/lib/core/services/api/courses_services.dart b/lib/core/services/api/courses_services.dart new file mode 100644 index 0000000..302a298 --- /dev/null +++ b/lib/core/services/api/courses_services.dart @@ -0,0 +1,29 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:pretty_dio_logger/pretty_dio_logger.dart'; + +class CoursesServices { + static String baseUrl = 'https://houshan.ai'; + static String getCourses = '/wp-json/wc/v3/products?per_page=50'; + static String getPosts = '/wp-json/wp/v2/posts?per_page=8'; + static const String _usernameAuth = + 'ck_4fdd186edb30c06f1c786473ec4d8bd216c9f4d6'; + static const String _passAuth = 'cs_e41e031f44197ecfef23bd685580d4b2650a9478'; + static const _canLog = kDebugMode && !kIsWeb; + + static getAuth() { + return { + 'Authorization': + 'Basic ${base64Encode(utf8.encode('$_usernameAuth:$_passAuth'))}', + }; + } + + static final Dio dio = Dio(BaseOptions( + baseUrl: baseUrl, + connectTimeout: const Duration(minutes: 1), + responseType: ResponseType.json, + )) + ..interceptors.add(PrettyDioLogger(enabled: _canLog)); +} diff --git a/lib/core/services/api/dio_service.dart b/lib/core/services/api/dio_service.dart new file mode 100644 index 0000000..eab864f --- /dev/null +++ b/lib/core/services/api/dio_service.dart @@ -0,0 +1,209 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/storage/shared_preferences_helper.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:pretty_dio_logger/pretty_dio_logger.dart'; + +class DioService { + static String baseURL = 'https://basa.houshan.ai'; + //user + static String sendOTP = '/v2/user/otp'; //POST + // static String register = '/user/register'; //POST + static String loginWithPassword = '/user/login'; //POST + static String loginWithOTP = '/user/login/otp'; //POST + static String getInfo = '/user/info'; //GET + static String editUsername = '/user/username'; //PUT + static String checkUsername = '/user/username'; //POST + static String editProfile = '/user/profile'; //PUT + static String editPassword = '/user/password'; //PUT + static String deleteProfile = '/user/profile'; //DELETE + static String giftCode = '/user/code'; //POST + static String cardNumber = '/user/card-number'; //PUT + static String addSubUser = '/user/sub-user'; //POST + static String getSubUsers = '/user/sub-user'; //GET + static String deleteSubUser(String id) => '/user/sub-user/$id'; //DELETE + static String reportDay = '/user/report/daily'; //GET + static String reportWeek = '/user/report/weekly'; //GET + static String reportPeriodic = '/user/report/periodic'; //GET + static String reportPiCoin = '/user/report/coin'; //GET + static String readAllNotifications = '/user/notification'; //PUT + static String fCMtoken = '/user/firebase-token'; //PUT + //chatbot + static String sendMessage = '/chatbot/'; //POST //GET //DELETE //STREAM + static String chatHistory({required final int id}) => + '/chatbot/$id'; //GET //DELETE q{id} + static String editTitle({required final int id}) => + '/chatbot/$id/title'; //PUT + static String archive({required final int id}) => + '/chatbot/$id/archive'; //PUT + static String relatedQuestions({required final int id}) => + '/chatbot/$id/related_questions'; //POST q{id}; + static String messageDelete( + {required final int id, required final String messageId}) => + '/chatbot/$id/message/$messageId'; //DELETE q{id} q{message_id} + static String likeMessage( + {required final int id, required final String messageId}) => + '/chatbot/$id/message/$messageId/feedback'; //PUT q{id} q{message_id} + //bot + static String getAllBots = '/bot/'; //GET q{string:serach} + static String getSingleBot(int id) => '/bot/$id'; //GET q{string:serach} + //category + static String sendMessageTool({required final int id}) => + '/tool/$id'; //POST //GET //DELETE //STREAM + static String allCategories = '/category/'; //GET + static String toolsCategories = '/v2/category/tool'; //GET + //Ticket + static String ticket = '/ticket/'; //GET + static String deleteTicket(int id) => '/ticket/$id'; //GET + //Forum + static String forumComments = '/forum/comment/'; //GET //POST + static String forumReplieComments(int id) => '/forum/comment/$id'; //GET + static String forumCommentsFeedback(int id) => + '/forum/comment/$id/feedback'; //PUT + //Assistant + static String getGlobalAssistants = '/bot/assistant'; //GET + static String getPersonalAssistants = '/bot/personal'; //GET + static String getPersonalAssistant(int id) => '/bot/personal/$id'; //GET + static String getGlobalAssistant(int id) => '/bot/$id'; //GET + static String getAssistantComments(int id) => '/bot/$id/comment'; //GET + static String markedBot(int id) => '/bot/$id/mark'; //PUT + //paymant + static String paymant = '/paymant/'; //GET + static String paymantBazar = '/paymant/bazzar'; //GET + static String paymantMyket = '/paymant/myket'; //GET + static String paymantHistory = '/paymant/history'; //GET + static String paymantPlans = '/v2/paymant/plan'; //GET + static String discount = '/discount/'; //POST + static String settlement = '/paymant/settlement'; //POST + static String homeBanner = '/banner/'; //GET + //generators + static String media = '/category/media'; //GET + static String effects = '/category/effect'; //GET + static String singleMedia(int id) => '/category/media/$id'; //GET + //characters + static String characters = '/category/character'; //GET + //events + static String events = '/event/'; + static String remaining = '/advertisement/remaining'; + DioService() { + if (kDebugMode) { + String token = AuthTokenStorage.getToken(); + print("AuthToken: $token"); + } + } + static const _canLog = kDebugMode && !kIsWeb; + + static final Dio _dio = Dio(BaseOptions( + baseUrl: baseURL, + connectTimeout: const Duration(minutes: 1), + responseType: ResponseType.json, + headers: { + "Content-Type": "application/json", + 'accept': '*/*', + // 'Authorization': "Bearer $token", + })) + ..interceptors.add(PrettyDioLogger(enabled: _canLog)); + + static final Dio _dioStream = Dio(BaseOptions( + baseUrl: baseURL, + connectTimeout: const Duration(minutes: 10), + receiveTimeout: const Duration(minutes: 10), + sendTimeout: const Duration(minutes: 10), + responseType: ResponseType.stream, + headers: { + "Content-Type": "application/json", + 'accept': '*/*', + })) + ..interceptors.add(PrettyDioLogger(enabled: _canLog)); + + Dio sendRequest() { + String token = AuthTokenStorage.getToken(); + _dio.options.headers.addAll({'Authorization': "Bearer $token"}); + return _dio; + } + + Dio sendRequestStream() { + String token = AuthTokenStorage.getToken(); + _dioStream.options.headers.addAll({'Authorization': "Bearer $token"}); + return _dioStream; + } + + static final Map> _ongoingDownloads = {}; + + static Future downloadFile(String url, + {String? cDirectory, Function(int, int)? onReceiveProgress}) async { + if (_ongoingDownloads.containsKey(url)) { + return _ongoingDownloads[url]; + } + + final downloadFuture = _downloadFile(url, + cDirectory: cDirectory, onReceiveProgress: onReceiveProgress); + _ongoingDownloads[url] = downloadFuture; + + final result = await downloadFuture; + _ongoingDownloads.remove(url); + return result; + } + + static Future _downloadFile(String url, + {String? cDirectory, Function(int, int)? onReceiveProgress}) async { + try { + final response = await _dio.get(url, + options: Options(responseType: ResponseType.bytes), + onReceiveProgress: onReceiveProgress); + if (response.statusCode == 200) { + final blob = response.data; + final fileName = url.split('/').last; + if (cDirectory != null) { + final filePath = '$cDirectory/$fileName'; + final file = File(filePath); + await file.writeAsBytes(blob); + return XFile(filePath); + } else { + final directory = await getApplicationDocumentsDirectory(); + final filePath = '${directory.path}/$fileName'; + final file = File(filePath); + await file.writeAsBytes(blob); + return XFile(filePath); + } + } else { + debugPrint( + "Error: Unable to download file. Status: ${response.statusCode}"); + return null; + } + } on DioException catch (e) { + if (kDebugMode) { + print("Error downloading file: $e"); + } + return null; + } + } + + static Future getMultipartFile(XFile file) async { + if (kIsWeb) { + final bytes = await file.readAsBytes(); + return MultipartFile.fromBytes(bytes, filename: file.name); + } else { + return MultipartFile.fromFile(file.path, filename: file.name); + } + } + + static Future getFileSize(String url) async { + try { + Dio dio = Dio(); + Response response = + await dio.head(url, options: Options(followRedirects: true)); + + String? contentLength = response.headers.value('content-length'); + return contentLength != null ? int.parse(contentLength) : null; + } catch (e) { + if (kDebugMode) { + print("Error: $e"); + } + return null; + } + } +} diff --git a/lib/core/services/downloader/downloader_service.dart b/lib/core/services/downloader/downloader_service.dart new file mode 100644 index 0000000..53d5c55 --- /dev/null +++ b/lib/core/services/downloader/downloader_service.dart @@ -0,0 +1,59 @@ +import 'dart:async'; + +import 'package:background_downloader/background_downloader.dart'; +import 'package:flutter/foundation.dart'; + +class DownloaderService { + static final Set activeDownloads = {}; + late DownloadTask task; + + ValueNotifier progressPer = ValueNotifier(0); + ValueNotifier onStatus = ValueNotifier(null); + + Future downloadFile(String url) async { + if (activeDownloads.contains(url)) { + return null; + } + + activeDownloads.add(url); + + var fileName = url.split('/').last; + if (!fileName.endsWith('.mp3')) { + fileName = '$fileName.mp3'; + } + task = DownloadTask( + url: url, + filename: fileName, + updates: Updates.statusAndProgress, + requiresWiFi: false, + retries: 5, + allowPause: true, + ); + + await FileDownloader().download( + task, + onProgress: (progress) { + progressPer.value = progress; + }, + onStatus: (status) { + onStatus.value = status; + }, + ); + activeDownloads.remove(url); + return (await task.filePath()); + } + + pauseDownload() async { + await FileDownloader().pause(task); + } + + cancelDownload() async { + await FileDownloader().cancelTaskWithId(task.taskId).whenComplete(() { + progressPer.value = 0; + }); + } + + resumeDownload() async { + await FileDownloader().resume(task); + } +} diff --git a/lib/core/services/file_manager/download_file_in_web.dart b/lib/core/services/file_manager/download_file_in_web.dart new file mode 100644 index 0000000..2a5c31d --- /dev/null +++ b/lib/core/services/file_manager/download_file_in_web.dart @@ -0,0 +1,16 @@ +import 'package:universal_html/html.dart' as html; +import 'dart:typed_data'; + +class DownloadFileInWeb { + static downloadFromUrl(String url, String fileName) { + html.AnchorElement(href: url) + ..setAttribute("download", fileName) + ..click(); + } + + static download(Uint8List uint8List, String fileName) { + final blob = html.Blob([uint8List]); + final url = html.Url.createObjectUrlFromBlob(blob); + downloadFromUrl(url, fileName); + } +} diff --git a/lib/core/services/file_manager/download_file_services.dart b/lib/core/services/file_manager/download_file_services.dart new file mode 100644 index 0000000..5bc8ffc --- /dev/null +++ b/lib/core/services/file_manager/download_file_services.dart @@ -0,0 +1,18 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_media_downloader/flutter_media_downloader.dart'; +import 'package:hoshan/core/services/file_manager/download_file_in_web.dart'; +import 'package:hoshan/core/services/permission/permission_service.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class DownloadFileService { + static Future getFile({required final String url}) async { + if (kIsWeb) { + DownloadFileInWeb.downloadFromUrl(url, url.split('/').last); + return; + } + await PermissionService.getPermission(permission: Permission.storage); + await PermissionService.getPermission(permission: Permission.notification); + MediaDownload().downloadFile(url, url.split('/').last, 'دانلود فایل', + '/storage/emulated/0/Download}'); + } +} diff --git a/lib/core/services/file_manager/excel_services.dart b/lib/core/services/file_manager/excel_services.dart new file mode 100644 index 0000000..c8bf72a --- /dev/null +++ b/lib/core/services/file_manager/excel_services.dart @@ -0,0 +1,85 @@ +import 'dart:io'; +import 'package:excel/excel.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hoshan/core/services/permission/permission_service.dart'; +import 'package:hoshan/core/utils/date_time.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class ExcelServices { + static const MethodChannel _channel = MethodChannel('file_channel'); + + static Future copyToDownloads(String filePath, String fileName) async { + await PermissionService.getPermission( + permission: Permission.manageExternalStorage); + + try { + final String result = await _channel.invokeMethod('copyToDownloads', { + 'filePath': filePath, + 'fileName': fileName, + }); + if (kDebugMode) { + print("File copied successfully: $result"); + } + } catch (e) { + if (kDebugMode) { + print("Error copying file: $e"); + } + rethrow; + } + } + + static Future saveDataTableToExcel( + List columns, List rows) async { + try { + // Create an Excel workbook + var excel = Excel.createExcel(); + + // Create a sheet + Sheet sheet = excel['Sheet1']; + + // Add column headers + List headers = columns.map((col) { + final label = col.label as Text; + return TextCellValue(label.data ?? ''); + }).toList(); + sheet.appendRow(headers); + + // Add row data + for (var row in rows) { + List rowData = row.cells.map((cell) { + final text = cell.child as Text; + return TextCellValue(text.data ?? ''); + }).toList(); + sheet.appendRow(rowData); + } + String sanitizedFileName = DateTimeUtils.getNow() + .toIso8601String() + .replaceAll(RegExp(r'[\/:*?"<>|]'), '_'); + if (kIsWeb) { + excel.save(fileName: '$sanitizedFileName.xlsx'); + + return 'success'; + } else { + await PermissionService.getPermission(permission: Permission.storage); + // Save the file + Directory? directory = await getDownloadsDirectory(); + String filePath = '${directory?.path}/$sanitizedFileName.xlsx'; + File(filePath) + ..createSync(recursive: true) + ..writeAsBytesSync(excel.save()!); + if (kDebugMode) { + print('Excel file saved at $filePath'); + } + return filePath; + } + } catch (e) { + if (kDebugMode) { + print('Excel file saved Error: $e'); + } + return null; + } + } +} diff --git a/lib/core/services/file_manager/pick_file_services.dart b/lib/core/services/file_manager/pick_file_services.dart new file mode 100644 index 0000000..d8c9949 --- /dev/null +++ b/lib/core/services/file_manager/pick_file_services.dart @@ -0,0 +1,115 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/core/services/permission/permission_service.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:image_picker_android/image_picker_android.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +// import 'package:image/image.dart' as img; + +class PickFileService { + BuildContext context; + + PickFileService(this.context); + Future?> getFile( + {required final FileType fileType, + bool allowMultiple = false, + final Function(FilePickerStatus)? onFileLoading, + final List? allowedExtensions, + final int? maxFiles, + final int? maxSize}) async { + try { + if (!kIsWeb) { + await PermissionService.getPermission(permission: Permission.storage); + } + FilePickerResult? result = await FilePicker.platform.pickFiles( + allowMultiple: allowMultiple, + type: fileType, + onFileLoading: onFileLoading, + allowedExtensions: allowedExtensions); + + if (result == null) return null; + + List files = result.xFiles; + final filteredFiles = []; + + for (var file in files) { + if (maxSize != null) { + int fileSize = + kIsWeb ? await file.length() : File(file.path).lengthSync(); + if (fileSize > maxSize * 1024 * 1024) { + SnackBarManager(context).show( + status: SnackBarStatus.error, + message: 'سایز فایل ${file.name} بیشتر از حد مجاز است!'); + continue; + } + } + if (allowedExtensions != null) { + if (!allowedExtensions.contains(file.name.split('.').last)) { + SnackBarManager(context).show( + status: SnackBarStatus.error, + message: 'پسوند فایل ${file.name} مجاز نیست!'); + continue; + } + } + filteredFiles.add(file); + } + + return filteredFiles; + } catch (e) { + if (kDebugMode) { + print("PickFileService Error: $e"); + } + return null; + } + } + + static Future getCameraImage() async { + await PermissionService.getPermission(permission: Permission.camera); + final ImagePickerPlatform imagePickerImplementation = + ImagePickerPlatform.instance; + if (imagePickerImplementation is ImagePickerAndroid) { + imagePickerImplementation.useAndroidPhotoPicker = true; + } + final ImagePicker picker = ImagePicker(); + final XFile? image = await picker.pickImage( + source: ImageSource.camera, + imageQuality: 20, + ); + + return image; + } + + static Future isImageValid(XFile file) async { + if (file.name.split('.').last.toLowerCase() != 'jpg' && + file.name.split('.').last.toLowerCase() != 'jpeg' && + file.name.split('.').last.toLowerCase() != 'png') { + return false; + } + + int fileSize = kIsWeb ? await file.length() : File(file.path).lengthSync(); + final validSize = 10; // MB + + if (fileSize > validSize * 1024 * 1024) { + return false; + } + + // final bytes = await file.readAsBytes(); + // final image = img.decodeImage(bytes); + // if (image == null) { + // return false; + // } + + // if (image.width > 2000 || image.height > 2000) { + // return false; + // } + + return true; + } +} diff --git a/lib/core/services/firebase/auth_service.dart b/lib/core/services/firebase/auth_service.dart new file mode 100644 index 0000000..34dfa6c --- /dev/null +++ b/lib/core/services/firebase/auth_service.dart @@ -0,0 +1,20 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:google_sign_in/google_sign_in.dart'; + +class AuthService { + //Google Sign In + Future signInWithGoogle() async { + final GoogleSignInAccount? gUser = await GoogleSignIn().signIn(); + final GoogleSignInAuthentication gAuth = await gUser!.authentication; + + final credential = GoogleAuthProvider.credential( + accessToken: gAuth.accessToken, + idToken: gAuth.idToken, + ); + + final userCredential = + await FirebaseAuth.instance.signInWithCredential(credential); + + return userCredential.user; + } +} diff --git a/lib/core/services/firebase/firebase_api.dart b/lib/core/services/firebase/firebase_api.dart new file mode 100644 index 0000000..134d277 --- /dev/null +++ b/lib/core/services/firebase/firebase_api.dart @@ -0,0 +1,58 @@ +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/data/model/chat_args.dart'; +import 'package:hoshan/main.dart'; + +class FirebasApi { + static final _firebaseMessaging = FirebaseMessaging.instance; + static Bots? bot; + static String? fcmToken; + + static Future initialNotifications() async { + await refreshToken(); + + initPushNotification(); + } + + static Future refreshToken() async { + await _firebaseMessaging.requestPermission(); + + fcmToken = await _firebaseMessaging.getToken(); + + if (kDebugMode) { + print('fCMToken: $fcmToken'); + } + } + + static Future deleteToken() async { + try { + await _firebaseMessaging.deleteToken(); + } catch (e) { + if (kDebugMode) { + print('Error while firebaseMessaging deleteToken: $e'); + } + } + } + + static void handleMessage(RemoteMessage? message) async { + if (message == null) return; + bot = Bots.fromNotification(message.data); + if (navigatorKey.currentState != null) { + if (bot!.tool ?? false) { + navigatorKey.currentContext?.go(Routes.assistant, extra: bot!.id); + } else { + navigatorKey.currentContext + ?.go(Routes.chat, extra: ChatArgs(bot: bot!)); + } + bot = null; + } + } + + static Future initPushNotification() async { + FirebaseMessaging.instance.getInitialMessage().then(handleMessage); + FirebaseMessaging.onMessageOpenedApp.listen(handleMessage); + } +} diff --git a/lib/core/services/permission/permission_service.dart b/lib/core/services/permission/permission_service.dart new file mode 100644 index 0000000..7d046fd --- /dev/null +++ b/lib/core/services/permission/permission_service.dart @@ -0,0 +1,22 @@ +import 'package:permission_handler/permission_handler.dart'; + +class PermissionService { + static Future getPermission( + {required final Permission permission}) async { + await permission.onDeniedCallback(() { + // Your code + }).onGrantedCallback(() { + // Your code + }).onPermanentlyDeniedCallback(() { + // Your code + }).onRestrictedCallback(() { + // Your code + }).onLimitedCallback(() { + // Your code + }).onProvisionalCallback(() { + // Your code + }).request(); + final status = await permission.status; + return status.isGranted; + } +} diff --git a/lib/core/services/webview/webview.dart b/lib/core/services/webview/webview.dart new file mode 100644 index 0000000..b09f598 --- /dev/null +++ b/lib/core/services/webview/webview.dart @@ -0,0 +1,13 @@ +import 'package:flutter/services.dart'; + +class NativeWebViewLauncher { + static const MethodChannel _channel = MethodChannel('file_channel'); + + static Future openWebView(String url) async { + try { + await _channel.invokeMethod('openWebView', {'url': url}); + } on PlatformException catch (e) { + print("Failed to open native webview: '${e.message}'."); + } + } +} diff --git a/lib/core/utils/crop_image.dart b/lib/core/utils/crop_image.dart new file mode 100644 index 0000000..f68aa0f --- /dev/null +++ b/lib/core/utils/crop_image.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:image_cropper/image_cropper.dart'; + +class CropImage { + Future getCroppedFile( + {required final BuildContext context, + required final String sourcePath, + final CropAspectRatioPresetData? aspectRatioPresets}) async { + SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( + statusBarColor: Colors.black, + statusBarIconBrightness: Brightness.light, + )); + + CroppedFile? croppedFile = await ImageCropper().cropImage( + sourcePath: sourcePath, + compressFormat: ImageCompressFormat.jpg, + uiSettings: [ + AndroidUiSettings( + toolbarTitle: 'برش عکس', + activeControlsWidgetColor: Theme.of(context).colorScheme.primary, + toolbarColor: Theme.of(context).colorScheme.primary, + statusBarColor: Colors.black, + toolbarWidgetColor: Colors.white, + backgroundColor: Colors.black, + dimmedLayerColor: Colors.black, + lockAspectRatio: true, + hideBottomControls: false, + showCropGrid: true, + initAspectRatio: aspectRatioPresets, + aspectRatioPresets: aspectRatioPresets != null + ? [aspectRatioPresets] + : const [ + CropAspectRatioPreset.original, + CropAspectRatioPreset.square, + CropAspectRatioPreset.ratio3x2, + CropAspectRatioPreset.ratio4x3, + CropAspectRatioPreset.ratio16x9 + ]), + IOSUiSettings( + title: 'برش عکس', + hidesNavigationBar: false, + ), + WebUiSettings( + context: context, + ), + ], + ); + + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Theme.of(context).brightness == Brightness.dark + ? Brightness.light + : Brightness.dark, + )); + + XFile? result; + if (croppedFile != null) { + result = XFile(croppedFile.path); + + final fileSize = await result.length(); + final fileSizeMb = (fileSize / 1024 / 1024); + + if (fileSizeMb > 2) { + result = await FlutterImageCompress.compressAndGetFile( + result.path, result.path, + format: CompressFormat.jpeg, quality: 20); + } else if (fileSizeMb > 5) { + result = await FlutterImageCompress.compressAndGetFile( + result.path, result.path, + format: CompressFormat.jpeg, quality: 40); + } else if (fileSizeMb > 8) { + result = await FlutterImageCompress.compressAndGetFile( + result.path, result.path, + format: CompressFormat.jpeg, quality: 60); + } + } + return result; + } +} + +class CropAspectRatioPresetCustom implements CropAspectRatioPresetData { + @override + (int, int)? get data => (1, 1); + + @override + String get name => '1x1 (customized)'; +} diff --git a/lib/core/utils/date_time.dart b/lib/core/utils/date_time.dart new file mode 100644 index 0000000..8b30436 --- /dev/null +++ b/lib/core/utils/date_time.dart @@ -0,0 +1,189 @@ +import 'package:flutter/foundation.dart'; +import 'package:intl/intl.dart'; +import 'package:shamsi_date/shamsi_date.dart'; + +class DateTimeUtils { + static DateTime getNow() { + try { + DateTime nowUtc = DateTime.now().toUtc(); + return nowUtc.add(const Duration(hours: 3, minutes: 30)); + } catch (e) { + // Handle the exception + if (kDebugMode) { + print('Error in getNow: $e'); + } + return DateTime.now(); + } + } + + static Jalali getNowJalali() { + return Jalali.now(); + } + + static String getTimeFromDuration(int seconds) { + try { + final time = Duration(seconds: seconds); + final min = time.inMinutes.toString().padLeft(2, '0'); + final sec = (time.inSeconds % 60).toString().padLeft(2, '0'); + return '$min:$sec'; + } catch (e) { + // Handle the exception + if (kDebugMode) { + print('Error in getTimeFromDuration: $e'); + } + return '00:00'; // Return a default value + } + } + + static String getFormattedTimeFromSeconds(int seconds) { + try { + if (seconds >= 3600) { + final hours = (seconds ~/ 3600).toString(); + final minutes = ((seconds % 3600) ~/ 60).toString().padLeft(2, '0'); + return '$hours:$minutes ساعت'; + } else if (seconds >= 60) { + final minutes = (seconds ~/ 60).toString(); + return '$minutes دقیقه'; + } else { + return '$seconds ثانیه'; + } + } catch (e) { + if (kDebugMode) { + print('Error in getFormattedTimeFromSeconds: $e'); + } + return '0 ثانیه'; // Default fallback + } + } + + static DateTime convertStringIsoToDate(String isoDate) { + try { + DateTime dateTime = DateTime.parse(isoDate); + DateTime iranTime = dateTime.add(const Duration(hours: 3, minutes: 30)); + return iranTime; + } catch (e) { + // Handle the exception + if (kDebugMode) { + print('Error in convertStringIsoToDate: $e'); + } + return DateTime.now(); // Return current time as a fallback + } + } + + static String convertDateToStringInFormatted(DateTime date, + {final String? formatted}) { + try { + String formattedDate = + DateFormat(formatted ?? 'yyyy-MM-dd – kk:mm').format(date); + return formattedDate; + } catch (e) { + // Handle the exception + if (kDebugMode) { + print('Error in convertDateToStringInFormatted: $e'); + } + return ''; // Return a default value + } + } + + static String convertStringIsoToStringInFormatted( + String isoDate, + ) { + try { + DateTime dateTime = convertStringIsoToDate(isoDate).toUtc() + ..add(const Duration(hours: 3, minutes: 30)); + final persianDate = Jalali.fromDateTime(dateTime); + return '${persianDate.year.toString().padLeft(2, '0')}-${persianDate.month.toString().padLeft(2, '0')}-${persianDate.day.toString().padLeft(2, '0')} - ${persianDate.hour.toString().padLeft(2, '0')}:${persianDate.minute.toString().padLeft(2, '0')} '; + } catch (e) { + // Handle the exception + if (kDebugMode) { + print('Error in convertStringIsoToStringInFormatted: $e'); + } + return ''; // Return a default value + } + } + + static String convertToSentTime(String isoDate) { + try { + final date = convertStringIsoToDate(isoDate); + final min = date.minute.toString().padLeft(2, '0'); + final hour = (date.hour % 60).toString().padLeft(2, '0'); + return '$hour:$min'; + } catch (e) { + // Handle the exception + if (kDebugMode) { + print('Error in convertToSentTime: $e'); + } + return '00:00'; // Return a default value + } + } + + static int getDaysBetweenNowAnd(String targetDateIso) { + try { + DateTime now = getNow(); + final targetDate = convertStringIsoToDate(targetDateIso); + Duration difference = now.difference(targetDate); + return difference.inDays; + } catch (e) { + // Handle the exception + if (kDebugMode) { + print('Error in getDaysBetweenNowAnd: $e'); + } + return 0; // Return a default value + } + } + + static int getDaysBetweenTwoDate(String firstDateIso, String targetDateIso) { + try { + DateTime now = convertStringIsoToDate(firstDateIso); + final targetDate = convertStringIsoToDate(targetDateIso); + Duration difference = now.difference(targetDate); + return difference.inDays; + } catch (e) { + // Handle the exception + if (kDebugMode) { + print('Error in getDaysBetweenNowAnd: $e'); + } + return 0; // Return a default value + } + } + + static Jalali getDateFromString(final bool isTime, final String date) { + try { + if (isTime) { + final d = getNow(); + return Jalali.fromDateTime(d).copy(hour: int.tryParse(date)); + } else { + final d = date.split('-'); + final dateInMiladi = DateTime(int.tryParse(d[0])!) + .copyWith(month: int.tryParse(d[1]), day: int.tryParse(d[2])); + final persianDate = Jalali.fromDateTime(dateInMiladi); + + return persianDate; + } + } catch (e) { + // Handle the exception + if (kDebugMode) { + print('Error in getDaysBetweenNowAnd: $e'); + } + final d = getNow(); + return Jalali.fromDateTime(d); + } + } + + static String normalizeTimeDuration(Duration input) { + String minute; + String second; + + if (input.inMinutes < 10) { + minute = '0${input.inMinutes}'; + } else { + minute = input.inMinutes.toString(); + } + int realSeconds = input.inSeconds % 60; + if (realSeconds < 10) { + second = '0$realSeconds'; + } else { + second = (realSeconds).toString(); + } + return '$minute:$second'; + } +} diff --git a/lib/core/utils/file.dart b/lib/core/utils/file.dart new file mode 100644 index 0000000..abf8b93 --- /dev/null +++ b/lib/core/utils/file.dart @@ -0,0 +1,44 @@ +import 'package:cross_file/cross_file.dart'; + +extension XFileType on XFile { + bool isImage() { + final extension = name.split('.').last.toLowerCase(); + const imageExtensions = [ + 'jpg', + 'jpeg', + 'png', + 'gif', + 'bmp', + 'webp', + 'tiff' + ]; + return imageExtensions.contains(extension); + } + + bool isDocument() { + final extension = name.split('.').last.toLowerCase(); + const documentExtensions = [ + 'pdf', + 'doc', + 'docx', + 'xls', + 'xlsx', + 'ppt', + 'pptx', + 'txt' + ]; + return documentExtensions.contains(extension); + } + + bool isAudio() { + final extension = name.split('.').last.toLowerCase(); + const audioExtensions = ['mp3', 'wav', 'aac', 'ogg', 'flac']; + return audioExtensions.contains(extension); + } + + bool isVideo() { + final extension = name.split('.').last.toLowerCase(); + const videoExtensions = ['mp4', 'avi', 'mov', 'wmv', 'mkv', 'flv']; + return videoExtensions.contains(extension); + } +} diff --git a/lib/core/utils/strings.dart b/lib/core/utils/strings.dart new file mode 100644 index 0000000..7bb8224 --- /dev/null +++ b/lib/core/utils/strings.dart @@ -0,0 +1,92 @@ +extension Allph on String { + bool startsWithEnglish() { + // Regular expression to check if the first character is an English letter + return RegExp(r'^[A-Za-z]').hasMatch(this); + } + + bool startsWithPersian() { + // Regular expression to check if the first character is a Persian letter + return RegExp(r'^[\u0600-\u06FF]').hasMatch(this); + } + + bool containsOnlyEnglish() { + return RegExp(r'^[a-zA-Z0-9@#$%^&*()_+-=!~/:;,.\|?]+$').hasMatch(this); + } + + bool containsOnlyPersianOrArabic() { + return RegExp(r'^[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\s]+$') + .hasMatch(this); + } + + bool isImage() { + final extension = split('.').last.toLowerCase(); + const imageExtensions = [ + 'jpg', + 'jpeg', + 'png', + 'gif', + 'bmp', + 'webp', + 'tiff' + ]; + return imageExtensions.contains(extension); + } + + bool isDocument() { + final extension = split('.').last.toLowerCase(); + const documentExtensions = [ + 'pdf', + 'doc', + 'docx', + 'xls', + 'xlsx', + 'ppt', + 'pptx', + 'txt' + ]; + return documentExtensions.contains(extension); + } + + bool isAudio() { + final extension = split('.').last.toLowerCase(); + const audioExtensions = ['mp3', 'wav', 'aac', 'ogg', 'flac']; + return audioExtensions.contains(extension); + } + + bool isVideo() { + final extension = split('.').last.toLowerCase(); + const videoExtensions = ['mp4', 'avi', 'mov', 'wmv', 'mkv', 'flv']; + return videoExtensions.contains(extension); + } + + String convertToEnglishNumber() { + final Map persianToEnglishMap = { + '۰': '0', + '۱': '1', + '۲': '2', + '۳': '3', + '۴': '4', + '۵': '5', + '۶': '6', + '۷': '7', + '۸': '8', + '۹': '9', + }; + + return split('').map((char) { + return persianToEnglishMap[char] ?? + char; // Replace with English number if exists, else keep original char + }).join(''); + } + + String? getFirstmageTag() { + RegExp exp = RegExp(r']+src="([^">]+)"'); + Match? match = exp.firstMatch(this); + + if (match != null) { + String imageUrl = match.group(1)!; + return imageUrl; + } + return null; + } +} diff --git a/lib/data/model/ai/ai_response_model.dart b/lib/data/model/ai/ai_response_model.dart new file mode 100644 index 0000000..3a62b2a --- /dev/null +++ b/lib/data/model/ai/ai_response_model.dart @@ -0,0 +1,79 @@ +class AiResponseModel { + int? chatId; + String? content; + String? aiMessageId; + String? humanMessageId; + String? chatTitle; + int? credit; + int? freeCredit; + bool? error; + int? statusCode; + String? detail; + + AiResponseModel( + {this.chatId, + this.content, + this.aiMessageId, + this.humanMessageId, + this.credit, + this.detail, + this.error, + this.freeCredit, + this.statusCode, + this.chatTitle}); + + AiResponseModel.fromJson(Map json) { + chatId = json['chat_id'] is String + ? int.tryParse(json['chat_id']) + : json['chat_id']; + chatTitle = json['chat_title']; + content = json['content']; + aiMessageId = json['ai_message_id']; + humanMessageId = json['human_message_id']; + credit = json['credit'] is String + ? int.tryParse(json['credit']) + : json['credit']; + error = json['error'] ?? false; + detail = json['detail']; + statusCode = json['status_code'] ?? 200; + freeCredit = + json['free'] is String ? int.tryParse(json['free']) : json['free']; + } + + Map toJson() { + final Map data = {}; + data['chat_id'] = chatId; + data['chat_title'] = chatTitle; + data['content'] = content; + data['ai_message_id'] = aiMessageId; + data['human_message_id'] = humanMessageId; + data['credit'] = credit; + return data; + } + + AiResponseModel copyWith({ + int? chatId, + String? content, + String? aiMessageId, + String? humanMessageId, + int? credit, + int? freeCredit, + String? chatTitle, + bool? error, + int? statusCode, + String? detail, + }) { + return AiResponseModel( + chatId: chatId ?? this.chatId, + content: content ?? this.content, + aiMessageId: aiMessageId ?? this.aiMessageId, + humanMessageId: humanMessageId ?? this.humanMessageId, + chatTitle: chatTitle ?? this.chatTitle, + credit: credit ?? this.credit, + error: error ?? this.error, + statusCode: statusCode ?? this.statusCode, + detail: detail ?? this.detail, + freeCredit: freeCredit ?? this.freeCredit, + ); + } +} diff --git a/lib/data/model/ai/bots_model.dart b/lib/data/model/ai/bots_model.dart new file mode 100644 index 0000000..696a827 --- /dev/null +++ b/lib/data/model/ai/bots_model.dart @@ -0,0 +1,252 @@ +import 'package:hoshan/data/model/tools_categories_model.dart'; + +class BotsModel { + List? bots; + + BotsModel({this.bots}); + + BotsModel.fromJson(Map json) { + if (json['bots'] != null) { + bots = []; + json['bots'].forEach((v) { + bots!.add(Bots.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + if (bots != null) { + data['bots'] = bots!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Bots { + int? id; + String? image; + String? image2; + String? model; + String? name; + int? cost; + int? limit; + bool? public; + int? attachment; + bool? marked; + bool? tool; + bool? deleted; + List? attachmentType; + + double? score; + int? comments; + int? messages; + int? users; + String? description; + String? createdAt; + String? type; + User? user; + Categories? category; + UserComment? userComment; + + Bots({ + this.id, + this.image, + this.image2, + this.model, + this.name, + this.cost, + this.limit, + this.public, + this.attachment, + this.marked, + this.tool, + this.deleted, + this.attachmentType, + this.score, + this.comments = 0, + this.messages, + this.users, + this.description, + this.createdAt, + this.user, + this.category, + this.userComment, + this.type, + }); + + Bots.fromNotification(Map json) { + id = int.parse(json['id']); + name = json['name']; + model = json['model']; + image = json['image']; + image2 = json['image_2']; + attachment = int.parse(json['attachment']); + tool = json['tool'].toString().toLowerCase() == 'true'; + deleted = json['deleted'].toString().toLowerCase() == 'true'; + cost = int.parse(json['cost']); + limit = int.parse(json['limit']); + public = json['public']; + type = json['type']; + marked = json['marked']; + if (json['attachment_type'] != null && + json['attachment_type'].toString().toLowerCase() != 'null') { + attachmentType = []; + json['attachment_type'].forEach((v) { + attachmentType!.add(v); + }); + } + score = json['score']; + comments = json['comments']; + users = json['users']; + messages = json['messages']; + description = json['description']; + createdAt = json['created_at']; + user = json['user'] != null ? User.fromJson(json['user']) : null; + category = + json['category'] != null ? Categories.fromJson(json['category']) : null; + userComment = json['user_comment'] != null + ? UserComment.fromJson(json['user_comment']) + : null; + } + + Bots.fromJson(Map json) { + id = json['id']; + name = json['name']; + model = json['model']; + image = json['image']; + image2 = json['image_2']; + attachment = json['attachment']; + tool = json['tool']; + deleted = json['deleted']; + type = json['type']; + cost = json['cost']; + limit = json['limit']; + public = json['public']; + marked = json['marked']; + if (json['attachment_type'] != null) { + attachmentType = []; + json['attachment_type'].forEach((v) { + attachmentType!.add(v); + }); + } + score = json['score']; + comments = json['comments']; + users = json['users']; + messages = json['messages']; + description = json['description']; + createdAt = json['created_at']; + user = json['user'] != null ? User.fromJson(json['user']) : null; + category = + json['category'] != null ? Categories.fromJson(json['category']) : null; + userComment = json['user_comment'] != null + ? UserComment.fromJson(json['user_comment']) + : null; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['name'] = name; + data['model'] = model; + data['image'] = image; + data['image_2'] = image2; + data['attachment'] = attachment; + data['tool'] = tool; + data['deleted'] = deleted; + data['cost'] = cost; + data['limit'] = limit; + data['public'] = public; + data['marked'] = marked; + data['attachment_type'] = attachmentType; + return data; + } + + Bots copyWith({ + int? id, + String? image, + String? image2, + String? model, + String? name, + int? cost, + int? limit, + bool? public, + int? attachment, + bool? marked, + bool? tool, + bool? deleted, + List? attachmentType, + double? score, + int? comments, + int? messages, + int? users, + String? description, + String? createdAt, + User? user, + Categories? category, + UserComment? userComment, + }) { + return Bots( + id: id ?? this.id, + image: image ?? this.image, + image2: image2 ?? this.image2, + model: model ?? this.model, + name: name ?? this.name, + cost: cost ?? this.cost, + limit: limit ?? this.limit, + public: public ?? this.public, + attachment: attachment ?? this.attachment, + marked: marked ?? this.marked, + tool: tool ?? this.tool, + deleted: deleted ?? this.deleted, + attachmentType: attachmentType ?? this.attachmentType, + score: score ?? this.score, + comments: comments ?? this.comments, + messages: messages ?? this.messages, + users: users ?? this.users, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + user: user ?? this.user, + category: category ?? this.category, + userComment: userComment ?? this.userComment, + ); + } +} + +class User { + String? name; + String? username; + + User({this.name, this.username}); + + User.fromJson(Map json) { + name = json['name']; + username = json['username']; + } + + Map toJson() { + final Map data = {}; + data['name'] = name; + data['username'] = username; + return data; + } +} + +class UserComment { + String? text; + double? score; + + UserComment({this.text, this.score}); + + UserComment.fromJson(Map json) { + text = json['text']; + score = json['score']; + } + + Map toJson() { + final Map data = {}; + data['text'] = text; + data['score'] = score; + return data; + } +} diff --git a/lib/data/model/ai/chats_history_model.dart b/lib/data/model/ai/chats_history_model.dart new file mode 100644 index 0000000..f527cc6 --- /dev/null +++ b/lib/data/model/ai/chats_history_model.dart @@ -0,0 +1,69 @@ +import 'package:hoshan/data/model/ai/bots_model.dart'; + +class ChatsHistoryModel { + List? chats; + int? page; + int? totalCount; + int? lastPage; + + ChatsHistoryModel({this.chats}); + + ChatsHistoryModel.fromJson(Map json) { + if (json['chats'] != null) { + chats = []; + json['chats'].forEach((v) { + chats!.add(Chats.fromJson(v)); + }); + } + page = json['page']; + totalCount = json['total_count']; + lastPage = json['last_page']; + } + + Map toJson() { + final Map data = {}; + if (chats != null) { + data['chats'] = chats!.map((v) => v.toJson()).toList(); + } + data['page'] = page; + data['total_count'] = totalCount; + data['last_page'] = lastPage; + return data; + } +} + +class Chats { + int? id; + String? title; + String? createdAt; + Bots? bot; + + Chats({this.id, this.title, this.createdAt, this.bot}); + + Chats.fromJson(Map json) { + id = json['id']; + title = json['title']; + createdAt = json['created_at']; + bot = json['bot'] != null ? Bots.fromJson(json['bot']) : null; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['title'] = title; + data['created_at'] = createdAt; + if (bot != null) { + data['bot'] = bot!.toJson(); + } + return data; + } + + Chats copyWith({int? id, String? title, String? createdAt, Bots? bot}) { + return Chats( + id: id ?? this.id, + title: title ?? this.title, + createdAt: createdAt ?? this.createdAt, + bot: bot ?? this.bot, + ); + } +} diff --git a/lib/data/model/ai/chats_indates_model.dart b/lib/data/model/ai/chats_indates_model.dart new file mode 100644 index 0000000..1229f46 --- /dev/null +++ b/lib/data/model/ai/chats_indates_model.dart @@ -0,0 +1,8 @@ +import 'package:hoshan/data/model/ai/chats_history_model.dart'; + +class ChatsIndatesModel { + final String title; + final List chats; + + ChatsIndatesModel({required this.title, required this.chats}); +} diff --git a/lib/data/model/ai/credit_model.dart b/lib/data/model/ai/credit_model.dart new file mode 100644 index 0000000..d506673 --- /dev/null +++ b/lib/data/model/ai/credit_model.dart @@ -0,0 +1,16 @@ +class CreditModel { + final int? credit; + final int? freeCredit; + + CreditModel({this.credit, this.freeCredit}); + + CreditModel copyWith({ + int? credit, + int? freeCredit, + }) { + return CreditModel( + credit: credit ?? this.credit, + freeCredit: freeCredit ?? this.freeCredit, + ); + } +} diff --git a/lib/data/model/ai/messages_model.dart b/lib/data/model/ai/messages_model.dart new file mode 100644 index 0000000..61c86cd --- /dev/null +++ b/lib/data/model/ai/messages_model.dart @@ -0,0 +1,216 @@ +import 'package:cross_file/cross_file.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; + +class MessagesModel { + int? id; + String? title; + String? createdAt; + Bots? bot; + List? messages; + + MessagesModel({this.id, this.title, this.createdAt, this.bot, this.messages}); + + MessagesModel.fromJson(Map json) { + id = json['id']; + title = json['title']; + createdAt = json['created_at']; + bot = json['bot'] != null ? Bots.fromJson(json['bot']) : null; + if (json['messages'] != null) { + messages = []; + json['messages'].forEach((v) { + messages!.add(Messages.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['title'] = title; + data['created_at'] = createdAt; + if (bot != null) { + data['bot'] = bot!.toJson(); + } + if (messages != null) { + data['messages'] = messages!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Messages { + String? id; + List? content; + String? query; + String? role; + String? fileUrl; + bool? like; + bool? fromBot; + bool? error; + XFile? file; + bool? retry; + String? createdAt; + + Messages( + {this.id, + this.content, + this.query, + this.role, + this.like, + this.file, + this.createdAt, + this.error = false, + this.retry = false}) { + fromBot = (role == 'ai'); + if (content != null && content!.isNotEmpty) { + // Sort content list: "image" first, then "text" + content!.sort((a, b) { + if (a.type == 'image_url' && b.type != 'image_url') { + return -1; // a comes before b + } else if (a.type != 'image_url' && b.type == 'image_url') { + return 1; // b comes before a + } + return 0; // maintain original order if both are the same type + }); + } + + // _getFile(); + } + + // Future _getFile() async { + // file = await ChatbotRepository.createXFileFromUrl(fileUrl ?? ''); + // } + + Messages.fromJson(Map json) { + error = false; + id = json['id']; + if (json['content'] != null) { + if (json['content'] is List) { + content = []; + json['content'].forEach((v) { + content!.add(Content.fromJson(v)); + }); + } else if (json['content'] is String) { + content = [Content(type: 'text', text: json['content'])]; + } + } + role = json['role']; + like = json['like']; + createdAt = json['created_at']; + + fromBot = role == 'ai'; + if (content != null && content!.isNotEmpty) { + // Sort content list: "image" first, then "text" + content!.sort((a, b) { + if (a.type == 'image_url' && b.type != 'image_url') { + return -1; // a comes before b + } else if (a.type != 'image_url' && b.type == 'image_url') { + return 1; // b comes before a + } + return 0; // maintain original order if both are the same type + }); + } + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + if (content != null) { + data['content'] = content!.map((v) => v.toJson()).toList(); + } + data['role'] = role; + data['like'] = like; + data['created_at'] = createdAt; + return data; + } + + Messages copyWith( + {String? id, + List? content, + String? role, + String? createdAt, + bool? like, + bool? error, + bool? retry, + String? query, + XFile? file}) { + return Messages( + id: id ?? this.id, + content: content ?? this.content, + like: like ?? this.like, + error: error ?? this.error, + role: role ?? this.role, + file: file ?? this.file, + createdAt: createdAt ?? this.createdAt, + query: query ?? this.query, + retry: retry ?? this.retry); + } +} + +class Content { + String? type; + String? text; + FileUrl? imageUrl; + FileUrl? audioUrl; + FileUrl? pdfUrl; + FileUrl? videoUrl; + + Content( + {this.type, + this.text, + this.imageUrl, + this.audioUrl, + this.pdfUrl, + this.videoUrl}); + + Content.fromJson(Map json) { + type = json['type']; + text = json['text']; + imageUrl = + json['image_url'] != null ? FileUrl.fromJson(json['image_url']) : null; + audioUrl = + json['audio_url'] != null ? FileUrl.fromJson(json['audio_url']) : null; + pdfUrl = json['pdf_url'] != null ? FileUrl.fromJson(json['pdf_url']) : null; + videoUrl = + json['video_url'] != null ? FileUrl.fromJson(json['video_url']) : null; + } + + Map toJson() { + final Map data = {}; + data['type'] = type; + data['text'] = text; + if (imageUrl != null) { + data['image_url'] = imageUrl!.toJson(); + } + if (audioUrl != null) { + data['audio_url'] = audioUrl!.toJson(); + } + if (pdfUrl != null) { + data['pdf_url'] = pdfUrl!.toJson(); + } + if (videoUrl != null) { + data['video_url'] = videoUrl!.toJson(); + } + return data; + } +} + +class FileUrl { + String? url; + String? query; + bool? attachment; + + FileUrl({this.url}); + FileUrl.fromJson(Map json) { + url = json['url']; + query = json['query']; + attachment = json['attachment']; + } + Map toJson() { + final Map data = {}; + data['url'] = url; + data['query'] = query; + data['attachment'] = attachment; + return data; + } +} diff --git a/lib/data/model/ai/related_questions_model.dart b/lib/data/model/ai/related_questions_model.dart new file mode 100644 index 0000000..039072f --- /dev/null +++ b/lib/data/model/ai/related_questions_model.dart @@ -0,0 +1,15 @@ +class RelatedQuestionsModel { + List? questions; + + RelatedQuestionsModel({this.questions}); + + RelatedQuestionsModel.fromJson(Map json) { + questions = json['questions'].cast(); + } + + Map toJson() { + final Map data = {}; + data['questions'] = questions; + return data; + } +} diff --git a/lib/data/model/ai/send_message_model.dart b/lib/data/model/ai/send_message_model.dart new file mode 100644 index 0000000..475136d --- /dev/null +++ b/lib/data/model/ai/send_message_model.dart @@ -0,0 +1,49 @@ +import 'package:cross_file/cross_file.dart'; + +class SendMessageModel { + int? id; + String? query; + String? option; + String? messageId; + int? botId; + bool? retry; + XFile? file; + bool? tool; + bool? ghost; + bool? webSearch; + + SendMessageModel( + {this.id, + this.query, + this.botId, + this.option, + this.retry, + this.file, + this.messageId, + this.ghost = false, + this.webSearch = false, + this.tool = false}); + + SendMessageModel.fromJson(Map json) { + id = json['id']; + query = json['query']; + botId = json['bot_id']; + retry = json['retry']; + option = json['options']; + ghost = json['ghost']; + ghost = json['ghost']; + webSearch = json['web_search']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['query'] = query; + data['options'] = option; + data['bot_id'] = botId; + data['retry'] = retry ?? false; + data['ghost'] = ghost ?? false; + data['web_search'] = webSearch ?? false; + return data; + } +} diff --git a/lib/data/model/assistant_comments_model.dart b/lib/data/model/assistant_comments_model.dart new file mode 100644 index 0000000..d53ecdf --- /dev/null +++ b/lib/data/model/assistant_comments_model.dart @@ -0,0 +1,80 @@ +class AssistantCommentsModel { + List? comments; + int? page; + int? totalCount; + int? lastPage; + + AssistantCommentsModel( + {this.comments, this.page, this.totalCount, this.lastPage}); + + AssistantCommentsModel.fromJson(Map json) { + if (json['comments'] != null) { + comments = []; + json['comments'].forEach((v) { + comments!.add(AssistantComments.fromJson(v)); + }); + } + page = json['page']; + totalCount = json['total_count']; + lastPage = json['last_page']; + } + + Map toJson() { + final Map data = {}; + if (comments != null) { + data['comments'] = comments!.map((v) => v.toJson()).toList(); + } + data['page'] = page; + data['total_count'] = totalCount; + data['last_page'] = lastPage; + return data; + } +} + +class AssistantComments { + String? text; + double? score; + String? createdAt; + AssistantCommentsUser? user; + + AssistantComments({this.text, this.score, this.createdAt, this.user}); + + AssistantComments.fromJson(Map json) { + text = json['text']; + score = json['score']; + createdAt = json['created_at']; + user = json['user'] != null + ? AssistantCommentsUser.fromJson(json['user']) + : null; + } + + Map toJson() { + final Map data = {}; + data['text'] = text; + data['score'] = score; + data['created_at'] = createdAt; + if (user != null) { + data['user'] = user!.toJson(); + } + return data; + } +} + +class AssistantCommentsUser { + String? username; + String? image; + + AssistantCommentsUser({this.username, this.image}); + + AssistantCommentsUser.fromJson(Map json) { + username = json['username']; + image = json['image']; + } + + Map toJson() { + final Map data = {}; + data['username'] = username; + data['image'] = image; + return data; + } +} diff --git a/lib/data/model/assistant_personal_info_model.dart b/lib/data/model/assistant_personal_info_model.dart new file mode 100644 index 0000000..d3fc4bf --- /dev/null +++ b/lib/data/model/assistant_personal_info_model.dart @@ -0,0 +1,76 @@ +import 'package:hoshan/data/model/tools_categories_model.dart'; + +class AssistantPersonalInfoModel { + AssistantPersonalInfo? assistantPersonalInfo; + + AssistantPersonalInfoModel({this.assistantPersonalInfo}); + + AssistantPersonalInfoModel.fromJson(Map json) { + assistantPersonalInfo = json['bot'] != null + ? AssistantPersonalInfo.fromJson(json['bot']) + : null; + } + + Map toJson() { + final Map data = {}; + if (assistantPersonalInfo != null) { + data['bot'] = assistantPersonalInfo!.toJson(); + } + return data; + } +} + +class AssistantPersonalInfo { + int? id; + String? image; + String? model; + String? name; + String? prompt; + String? description; + bool? public; + List? links; + List? docs; + Categories? category; + + AssistantPersonalInfo( + {this.id, + this.image, + this.model, + this.name, + this.prompt, + this.description, + this.public, + this.links, + this.category}); + + AssistantPersonalInfo.fromJson(Map json) { + id = json['id']; + image = json['image']; + model = json['model']; + name = json['name']; + prompt = json['prompt']; + description = json['description']; + public = json['public']; + links = json['links'] != null ? json['links'].cast() : []; + docs = json['docs'] != null ? json['docs'].cast() : []; + category = + json['category'] != null ? Categories.fromJson(json['category']) : null; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['image'] = image; + data['model'] = model; + data['name'] = name; + data['prompt'] = prompt; + data['description'] = description; + data['public'] = public; + data['links'] = links; + data['docs'] = docs; + if (category != null) { + data['category'] = category!.toJson(); + } + return data; + } +} diff --git a/lib/data/model/assistants_args.dart b/lib/data/model/assistants_args.dart new file mode 100644 index 0000000..5a2ed3e --- /dev/null +++ b/lib/data/model/assistants_args.dart @@ -0,0 +1,8 @@ +class AssistantsArgs { + final int id; + final int catId; + final bool personal; + + AssistantsArgs( + {required this.id, required this.catId, required this.personal}); +} diff --git a/lib/data/model/auth/auth_screens_enum.dart b/lib/data/model/auth/auth_screens_enum.dart new file mode 100644 index 0000000..fe63beb --- /dev/null +++ b/lib/data/model/auth/auth_screens_enum.dart @@ -0,0 +1 @@ +enum AuthScreens { mobile, verification, code } diff --git a/lib/data/model/auth/login_model.dart b/lib/data/model/auth/login_model.dart new file mode 100644 index 0000000..3fdcbc3 --- /dev/null +++ b/lib/data/model/auth/login_model.dart @@ -0,0 +1,21 @@ +class LoginModel { + String? detail; + String? accessToken; + String? tokenType; + + LoginModel({this.detail, this.accessToken, this.tokenType}); + + LoginModel.fromJson(Map json) { + detail = json['detail']; + accessToken = json['access_token']; + tokenType = json['token_type']; + } + + Map toJson() { + final Map data = {}; + data['detail'] = detail; + data['access_token'] = accessToken; + data['token_type'] = tokenType; + return data; + } +} diff --git a/lib/data/model/auth/user_info_model.dart b/lib/data/model/auth/user_info_model.dart new file mode 100644 index 0000000..ad9b903 --- /dev/null +++ b/lib/data/model/auth/user_info_model.dart @@ -0,0 +1,105 @@ +class UserInfoModel { + String? mobileNumber; + String? id; + String? name; + String? image; + String? email; + String? username; + String? code; + int? credit; + double? income; + int? freeCredit; + String? cardNumber; + dynamic parent; + bool? login; + int? gift_credit; + List? notifications; + + UserInfoModel( + {this.mobileNumber, + this.id, + this.name, + this.image, + this.email, + this.username, + this.code, + this.credit, + this.freeCredit, + this.login = false, + this.cardNumber, + this.parent, + this.income, + this.gift_credit, + this.notifications}); + + UserInfoModel.fromJson(Map json) { + mobileNumber = json['mobile_number']; + id = json['id']; + name = json['name']; + image = json['image']; + email = json['email']; + username = json['username']; + code = json['code']; + credit = json['credit']; + freeCredit = json['free_credit']; + cardNumber = (json['card_number'] as String?) != null && + (json['card_number'] as String?)!.isEmpty + ? null + : json['card_number']; + income = json['income']; + parent = json['parent']; + if (json['notifications'] != null) { + notifications = []; + json['notifications'].forEach((v) { + notifications!.add(Notifications.fromJson(v)); + }); + } + login = true; + gift_credit = json['gift_credit']; + } + + Map toJson() { + final Map data = {}; + data['mobile_number'] = mobileNumber; + data['id'] = id; + data['name'] = name; + data['image'] = image; + data['email'] = email; + data['username'] = username; + data['code'] = code; + data['credit'] = credit; + data['free_credit'] = freeCredit; + data['card_number'] = cardNumber; + data['parent'] = parent; + if (notifications != null) { + data['notifications'] = notifications!.map((v) => v.toJson()).toList(); + } + data['gift_credit'] = gift_credit; + return data; + } +} + +class Notifications { + String? title; + String? message; + bool? seen; + String? createdAt; + + Notifications({this.title, this.message, this.seen, this.createdAt}); + + Notifications.fromJson(Map json) { + title = json['title']; + message = json['message']; + seen = json['seen']; + createdAt = json['created_at']; + } + + Map toJson() { + final Map data = {}; + data['title'] = title; + data['message'] = message; + data['seen'] = seen; + data['created_at'] = createdAt; + return data; + } +} diff --git a/lib/data/model/banner_model.dart b/lib/data/model/banner_model.dart new file mode 100644 index 0000000..a1598bc --- /dev/null +++ b/lib/data/model/banner_model.dart @@ -0,0 +1,75 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/ui/theme/colors.dart'; + +List> _listOfColors = [ + [ + AppColors.primaryColor[200].withAlpha(80), + AppColors.primaryColor[200].withAlpha(80), + AppColors.primaryColor[300], + AppColors.primaryColor[400], + AppColors.primaryColor.defaultShade, + ], + [ + AppColors.secondryColor[200].withAlpha(50), + AppColors.secondryColor[200].withAlpha(80), + AppColors.secondryColor[300], + AppColors.secondryColor[400], + AppColors.secondryColor.defaultShade + ], + [ + const Color(0xffbbf7d0).withAlpha(50), + const Color(0xff9be0b7).withAlpha(80), + const Color(0xff57b488), + const Color(0xff2d9f72), + const Color(0xff059467), + ] +]; + +class BannerModel { + final Widget child; + final String imageUrl; + final List colors; + BannerModel( + {required this.child, required this.imageUrl, required this.colors}); +} + +class Banners { + String? image; + String? title; + String? description; + String? link; + List? colors; + Bots? bot; + + Banners({this.image, this.title, this.description, this.colors}) { + if (colors == null) { + final randomIndex = Random().nextInt(_listOfColors.length); + colors = _listOfColors[randomIndex]; + } + } + + Banners.fromJson(Map json) { + image = json['image']; + title = json['title']; + link = json['link']; + description = json['description']; + final randomIndex = Random().nextInt(_listOfColors.length); + colors = _listOfColors[randomIndex]; + bot = json['bot'] != null ? Bots.fromJson(json['bot']) : null; + } + + Map toJson() { + final Map data = {}; + data['image'] = image; + data['title'] = title; + data['link'] = link; + data['description'] = description; + if (bot != null) { + data['bot'] = bot!.toJson(); + } + return data; + } +} diff --git a/lib/data/model/billings_history_model.dart b/lib/data/model/billings_history_model.dart new file mode 100644 index 0000000..adb1491 --- /dev/null +++ b/lib/data/model/billings_history_model.dart @@ -0,0 +1,70 @@ +class BillingsHistoryModel { + List? billings; + + BillingsHistoryModel({this.billings}); + + BillingsHistoryModel.fromJson(Map json) { + if (json['billings'] != null) { + billings = []; + json['billings'].forEach((v) { + billings!.add(Billings.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + if (billings != null) { + data['billings'] = billings!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class PaginationBillings { + final int page; + final List billings; + + PaginationBillings({required this.page, required this.billings}); +} + +class Billings { + int? id; + int? credit; + int? amount; + String? refId; + String? type; + String? status; + String? createdAt; + + Billings( + {this.id, + this.credit, + this.amount, + this.refId, + this.type, + this.status, + this.createdAt}); + + Billings.fromJson(Map json) { + id = json['id']; + credit = json['credit']; + amount = json['amount']; + refId = json['ref_id']; + type = json['type']; + status = json['status']; + createdAt = json['created_at']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['credit'] = credit; + data['amount'] = amount; + data['ref_id'] = refId; + data['type'] = type; + data['status'] = status; + data['created_at'] = createdAt; + return data; + } +} diff --git a/lib/data/model/chat_args.dart b/lib/data/model/chat_args.dart new file mode 100644 index 0000000..1b9c305 --- /dev/null +++ b/lib/data/model/chat_args.dart @@ -0,0 +1,12 @@ +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/data/model/ai/messages_model.dart'; + +class ChatArgs { + final int? chatId; + final Bots bot; + final Messages? messages; + final bool isPerson; + + ChatArgs( + {this.chatId, this.messages, required this.bot, this.isPerson = false}); +} diff --git a/lib/data/model/courses_model.dart b/lib/data/model/courses_model.dart new file mode 100644 index 0000000..9514ff6 --- /dev/null +++ b/lib/data/model/courses_model.dart @@ -0,0 +1,107 @@ +class Courses { + int? id; + String? name; + String? permalink; + String? dateCreated; + String? shortDescription; + String? price; + String? regularPrice; + String? salePrice; + List? images; + List? categories; + + Courses({ + this.id, + this.name, + this.permalink, + this.dateCreated, + this.shortDescription, + this.price, + this.regularPrice, + this.salePrice, + this.images, + this.categories, + }); + + factory Courses.fromJson(Map json) { + return Courses( + id: json['id'], + name: json['name'], + permalink: json['permalink'], + dateCreated: json['date_created'], + shortDescription: json['short_description'], + price: json['price'], + regularPrice: json['regular_price'], + salePrice: json['sale_price'], + images: json['images'] != null + ? (json['images'] as List) + .map((i) => CourseImage.fromJson(i)) + .toList() + : null, + categories: json['categories'] != null + ? (json['categories'] as List) + .map((i) => Category.fromJson(i)) + .toList() + : null, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'permalink': permalink, + 'date_created': dateCreated, + 'short_description': shortDescription, + 'price': price, + 'regular_price': regularPrice, + 'sale_price': salePrice, + 'images': images?.map((i) => i.toJson()).toList(), + }; + } +} + +class Category { + int? id; + + Category({this.id}); + + factory Category.fromJson(Map json) { + return Category( + id: json['id'], + ); + } + + Map toJson() { + return { + 'id': id, + }; + } +} + +class CourseImage { + int? id; + String? src; + String? name; + String? alt; + + CourseImage({this.id, this.src, this.name, this.alt}); + + factory CourseImage.fromJson(Map json) { + return CourseImage( + id: json['id'], + src: json['src'], + name: json['name'], + alt: json['alt'], + ); + } + + Map toJson() { + return { + 'id': id, + 'src': src, + 'name': name, + 'alt': alt, + }; + } +} diff --git a/lib/data/model/create_assistant_request_model.dart b/lib/data/model/create_assistant_request_model.dart new file mode 100644 index 0000000..133e917 --- /dev/null +++ b/lib/data/model/create_assistant_request_model.dart @@ -0,0 +1,54 @@ +import 'package:cross_file/cross_file.dart'; +import 'package:dio/dio.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; + +class CreateAssistantRequestModel { + // final int botId; + final int categoryId; + final String name; + final String description; + final String prompt; + final bool public; + // final XFile image; + final List? links; + final List? files; + + CreateAssistantRequestModel({ + // required this.botId, + required this.categoryId, + required this.name, + required this.description, + required this.prompt, + required this.public, + // required this.image, + this.links, + this.files, + }); + + Future toFormData() async { + FormData formDatBody = FormData(); + + // formDatBody.fields.add(MapEntry('bot_id', botId.toString())); + formDatBody.fields.add(MapEntry('category_id', categoryId.toString())); + formDatBody.fields.add(MapEntry('name', name.toString())); + formDatBody.fields.add(MapEntry('description', description.toString())); + formDatBody.fields.add(MapEntry('prompt', prompt.toString())); + formDatBody.fields.add(MapEntry('public', public.toString())); + // MultipartFile multipartFile = await DioService.getMultipartFile(image); + + // formDatBody.files.add(MapEntry('image', multipartFile)); + if (links != null && links!.isNotEmpty) { + for (var link in links!) { + formDatBody.fields.add(MapEntry('links', link.toString())); + } + } + if (files != null && files!.isNotEmpty) { + for (var file in files!) { + MultipartFile multipartFile = await DioService.getMultipartFile(file); + formDatBody.files.add(MapEntry('docs', multipartFile)); + } + } + + return formDatBody; + } +} diff --git a/lib/data/model/discount_model.dart b/lib/data/model/discount_model.dart new file mode 100644 index 0000000..c4aeb8a --- /dev/null +++ b/lib/data/model/discount_model.dart @@ -0,0 +1,24 @@ +class DiscountModel { + int? percent; + int? value; + int? maxValue; + String? bazzarToken; + + DiscountModel({this.percent, this.value, this.maxValue, this.bazzarToken}); + + DiscountModel.fromJson(Map json) { + percent = json['percent']; + value = json['value']; + maxValue = json['maxValue']; + bazzarToken = json['bazzar_token']; + } + + Map toJson() { + final Map data = {}; + data['percent'] = percent; + data['value'] = value; + data['maxValue'] = maxValue; + data['bazzar_token'] = bazzarToken; + return data; + } +} diff --git a/lib/data/model/edittext_state_model.dart b/lib/data/model/edittext_state_model.dart new file mode 100644 index 0000000..ae9708b --- /dev/null +++ b/lib/data/model/edittext_state_model.dart @@ -0,0 +1,15 @@ +import 'package:flutter/cupertino.dart'; + +class EdittextStateModel { + final GlobalKey formState = GlobalKey(); + final TextEditingController formController = TextEditingController(); + final String? hintText; + final String? tooltipHint; + final String? label; + + EdittextStateModel({this.hintText, this.tooltipHint, this.label}); + + EdittextStateModel copyWith({final String? hintText}) { + return EdittextStateModel(hintText: hintText ?? this.hintText); + } +} diff --git a/lib/data/model/effects_model.dart b/lib/data/model/effects_model.dart new file mode 100644 index 0000000..7ca0eaa --- /dev/null +++ b/lib/data/model/effects_model.dart @@ -0,0 +1,41 @@ +class EffectsModel { + List? effects; + + EffectsModel({this.effects}); + + EffectsModel.fromJson(Map json) { + if (json['effects'] != null) { + effects = []; + json['effects'].forEach((v) { + effects!.add(Effects.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + if (effects != null) { + data['effects'] = effects!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Effects { + String? name; + String? gif; + + Effects({this.name, this.gif}); + + Effects.fromJson(Map json) { + name = json['name']; + gif = json['gif']; + } + + Map toJson() { + final Map data = {}; + data['name'] = name; + data['gif'] = gif; + return data; + } +} diff --git a/lib/data/model/empty_states_enum.dart b/lib/data/model/empty_states_enum.dart new file mode 100644 index 0000000..beffd96 --- /dev/null +++ b/lib/data/model/empty_states_enum.dart @@ -0,0 +1,19 @@ +import 'package:hoshan/core/gen/assets.gen.dart'; + +enum EmptyStatesEnum { + messages, + archive, + connection, + server, + amount, + inbox, + assistant, + familyMembers +} + +class EmptyStateModel { + final AssetGenImage image; + final String title; + + EmptyStateModel({required this.image, required this.title}); +} \ No newline at end of file diff --git a/lib/data/model/event_model.dart b/lib/data/model/event_model.dart new file mode 100644 index 0000000..d068e25 --- /dev/null +++ b/lib/data/model/event_model.dart @@ -0,0 +1,109 @@ +class EventModel { + List? events; + + EventModel({this.events}); + + EventModel.fromJson(Map json) { + if (json['events'] != null) { + events = []; + json['events'].forEach((v) { + events!.add(Events.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + if (events != null) { + data['events'] = events!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Events { + int? id; + String? title; + String? description; + String? subtitle; + String? awards; + String? image; + String? startAt; + String? endAt; + bool? isOpen; + int? totalParticipants; + int? totalReceivedWorks; + List? winners; + + Events( + {this.id, + this.title, + this.description, + this.subtitle, + this.awards, + this.image, + this.startAt, + this.endAt, + this.isOpen, + this.totalParticipants, + this.totalReceivedWorks, + this.winners}); + + Events.fromJson(Map json) { + id = json['id']; + title = json['title']; + description = json['description']; + subtitle = json['subtitle']; + awards = json['awards']; + image = json['image']; + startAt = json['start_at']; + endAt = json['end_at']; + isOpen = json['is_open']; + totalParticipants = json['total_participants']; + totalReceivedWorks = json['total_received_works']; + if (json['winners'] != null) { + winners = []; + json['winners'].forEach((v) { + winners!.add(Winners.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['title'] = title; + data['description'] = description; + data['subtitle'] = subtitle; + data['awards'] = awards; + data['image'] = image; + data['start_at'] = startAt; + data['end_at'] = endAt; + data['is_open'] = isOpen; + data['total_participants'] = totalParticipants; + data['total_received_works'] = totalReceivedWorks; + if (winners != null) { + data['winners'] = winners!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Winners { + int? rank; + String? username; + + Winners({this.rank, this.username}); + + Winners.fromJson(Map json) { + rank = json['rank']; + username = json['username']; + } + + Map toJson() { + final Map data = {}; + data['rank'] = rank; + data['username'] = username; + return data; + } +} diff --git a/lib/data/model/forum_model.dart b/lib/data/model/forum_model.dart new file mode 100644 index 0000000..c1579a9 --- /dev/null +++ b/lib/data/model/forum_model.dart @@ -0,0 +1,140 @@ +class ForumModel { + List? comments; + List? replies; + int? page; + int? totalCount; + int? lastPage; + + ForumModel({this.comments, this.page, this.totalCount, this.lastPage}); + + ForumModel.fromJson(Map json) { + if (json['comments'] != null) { + comments = []; + json['comments'].forEach((v) { + comments!.add(Comment.fromJson(v)); + }); + } + if (json['replies'] != null) { + replies = []; + json['replies'].forEach((v) { + replies!.add(Comment.fromJson(v)); + }); + } + page = json['page']; + totalCount = json['total_count']; + lastPage = json['last_page']; + } + + Map toJson() { + final Map data = {}; + if (comments != null) { + data['comments'] = comments!.map((v) => v.toJson()).toList(); + } + if (replies != null) { + data['replies'] = replies!.map((v) => v.toJson()).toList(); + } + data['page'] = page; + data['total_count'] = totalCount; + data['last_page'] = lastPage; + return data; + } +} + +class Comment { + int? id; + String? text; + String? image; + String? createdAt; + int? replies; + int? likes; + int? dislikes; + int? userFeedback; + User? user; + + Comment( + {this.id, + this.text, + this.image, + this.createdAt, + this.replies = 0, + this.likes = 0, + this.dislikes = 0, + this.userFeedback, + this.user}); + + Comment.fromJson(Map json) { + id = json['id']; + text = json['text']; + image = json['image']; + createdAt = json['created_at']; + replies = json['replies'] ?? 0; + likes = json['likes'] ?? 0; + dislikes = json['dislikes'] ?? 0; + userFeedback = json['user_feedback']; + user = json['user'] != null ? User.fromJson(json['user']) : null; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['text'] = text; + data['image'] = image; + data['created_at'] = createdAt; + data['replies'] = replies; + data['likes'] = likes; + data['dislikes'] = dislikes; + data['user_feedback'] = userFeedback; + if (user != null) { + data['user'] = user!.toJson(); + } + return data; + } + + Comment copyWith( + {int? id, + String? text, + String? image, + String? createdAt, + int? replies, + int? likes, + int? dislikes, + int? userFeedback, + User? user}) { + return Comment( + id: id ?? this.id, + text: text ?? this.text, + image: image ?? this.image, + replies: replies ?? this.replies, + likes: likes ?? this.likes, + dislikes: dislikes ?? this.dislikes, + userFeedback: userFeedback ?? this.userFeedback, + createdAt: createdAt ?? this.createdAt, + user: user ?? this.user, + ); + } +} + +class User { + String? id; + String? name; + String? image; + String? username; + + User({this.id, this.name, this.image, this.username}); + + User.fromJson(Map json) { + id = json['id']; + name = json['name']; + image = json['image']; + username = json['username']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['name'] = name; + data['image'] = image; + data['username'] = username; + return data; + } +} diff --git a/lib/data/model/global_assistant_bots_model.dart b/lib/data/model/global_assistant_bots_model.dart new file mode 100644 index 0000000..d5b7f80 --- /dev/null +++ b/lib/data/model/global_assistant_bots_model.dart @@ -0,0 +1,50 @@ +import 'package:hoshan/data/model/ai/bots_model.dart'; + +class GlobalAssistantBotsModel { + List? categories; + + GlobalAssistantBotsModel({this.categories}); + + GlobalAssistantBotsModel.fromJson(Map json) { + if (json['categories'] != null) { + categories = []; + json['categories'].forEach((v) { + categories!.add(GlobalAssistant.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + if (categories != null) { + data['categories'] = categories!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class GlobalAssistant { + String? categoryName; + List? bots; + + GlobalAssistant({this.categoryName, this.bots}); + + GlobalAssistant.fromJson(Map json) { + categoryName = json['category_name']; + if (json['bots'] != null) { + bots = []; + json['bots'].forEach((v) { + bots!.add(Bots.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + data['category_name'] = categoryName; + if (bots != null) { + data['bots'] = bots!.map((v) => v.toJson()).toList(); + } + return data; + } +} diff --git a/lib/data/model/home_args.dart b/lib/data/model/home_args.dart new file mode 100644 index 0000000..c1b2956 --- /dev/null +++ b/lib/data/model/home_args.dart @@ -0,0 +1,6 @@ +class HomeArgs { + final int? freeCredit; + final String? message; + + HomeArgs({this.freeCredit, this.message}); +} diff --git a/lib/data/model/home_navbar_model.dart b/lib/data/model/home_navbar_model.dart new file mode 100644 index 0000000..9d1f5a8 --- /dev/null +++ b/lib/data/model/home_navbar_model.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; + +class HomeNavbar { + final String title; + final String icon; + bool enabled; + final String outlineAsset = 'assets/icon/navbars/navigation/'; + String bulkAsset(String theme) => 'assets/icon/navbars/navigation-$theme/'; + HomeNavbar({required this.title, required this.icon, required this.enabled}); + + SvgGenImage getIcon(BuildContext context) { + final path = + '${enabled ? bulkAsset(context.read().isDark() ? 'dark' : 'light') : outlineAsset}$icon.svg'; + return SvgGenImage(path, size: const Size(24, 24)); + } + + void setEnabled(bool enable) { + enabled = enable; + } +} diff --git a/lib/data/model/media_model.dart b/lib/data/model/media_model.dart new file mode 100644 index 0000000..bfe9e63 --- /dev/null +++ b/lib/data/model/media_model.dart @@ -0,0 +1,47 @@ +class MediasModel { + List? categories; + + MediasModel({this.categories}); + + MediasModel.fromJson(Map json) { + if (json['categories'] != null) { + categories = []; + json['categories'].forEach((v) { + categories!.add(Categories.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + if (categories != null) { + data['categories'] = categories!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Categories { + int? id; + String? name; + String? icon; + String? image; + + Categories({this.id, this.name, this.icon, this.image}); + + Categories.fromJson(Map json) { + id = json['id']; + name = json['name']; + icon = json['icon']; + image = json['image']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['name'] = name; + data['icon'] = icon; + data['image'] = image; + return data; + } +} diff --git a/lib/data/model/on_boarding_slider.dart b/lib/data/model/on_boarding_slider.dart new file mode 100644 index 0000000..e687b9f --- /dev/null +++ b/lib/data/model/on_boarding_slider.dart @@ -0,0 +1,14 @@ +import 'package:hoshan/core/gen/assets.gen.dart'; + +class OnBoardingSlider { + final int id; + final AssetGenImage image; + final String title; + final String description; + + OnBoardingSlider( + {required this.id, + required this.image, + required this.title, + required this.description}); +} diff --git a/lib/data/model/personal_assistants_bots.dart b/lib/data/model/personal_assistants_bots.dart new file mode 100644 index 0000000..86525cd --- /dev/null +++ b/lib/data/model/personal_assistants_bots.dart @@ -0,0 +1,63 @@ +class PersonalAssistantBotsModel { + List? personalAssistants; + + PersonalAssistantBotsModel({this.personalAssistants}); + + PersonalAssistantBotsModel.fromJson(Map json) { + if (json['bots'] != null) { + personalAssistants = []; + json['bots'].forEach((v) { + personalAssistants!.add(PersonalAssistant.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + if (personalAssistants != null) { + data['bots'] = personalAssistants!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class PersonalAssistant { + int? id; + String? name; + String? image; + double? score; + int? comments; + String? status; + String? description; + + PersonalAssistant( + {this.id, + this.name, + this.image, + this.score, + this.comments, + this.status, + this.description}); + + PersonalAssistant.fromJson(Map json) { + id = json['id']; + name = json['name']; + image = json['image']; + score = json['score']; + comments = json['comments']; + status = json['status']; + description = json['description']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['name'] = name; + data['image'] = image; + data['score'] = score; + data['comments'] = comments; + data['status'] = status; + data['description'] = description; + return data; + } +} diff --git a/lib/data/model/photo_gen_model.dart b/lib/data/model/photo_gen_model.dart new file mode 100644 index 0000000..ede8ea8 --- /dev/null +++ b/lib/data/model/photo_gen_model.dart @@ -0,0 +1,62 @@ +import 'package:hoshan/data/model/ai/bots_model.dart'; + +class GensModel { + List? categories; + + GensModel({this.categories}); + + GensModel.fromJson(Map json) { + if (json['categories'] != null) { + categories = []; + json['categories'].forEach((v) { + categories!.add(GenModel.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + if (categories != null) { + data['categories'] = categories!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class GenModel { + int? id; + String? name; + String? image; + String? icon; + String? description; + List? bots; + + GenModel({this.id, this.name, this.image, this.description, this.bots}); + + GenModel.fromJson(Map json) { + id = json['id']; + name = json['name']; + image = json['image']; + icon = json['icon']; + description = json['description']; + if (json['bots'] != null) { + bots = []; + json['bots'].forEach((v) { + bots!.add(Bots.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['name'] = name; + data['image'] = image; + data['icon'] = icon; + data['description'] = description; + if (bots != null) { + data['bots'] = bots!.map((v) => v.toJson()).toList(); + } + return data; + } +} diff --git a/lib/data/model/plans_model.dart b/lib/data/model/plans_model.dart new file mode 100644 index 0000000..3d14245 --- /dev/null +++ b/lib/data/model/plans_model.dart @@ -0,0 +1,65 @@ +class PlansModel { + List? plans; + + PlansModel({this.plans}); + + PlansModel.fromJson(Map json) { + if (json['plans'] != null) { + plans = []; + json['plans'].forEach((v) { + plans!.add(Plans.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + if (plans != null) { + data['plans'] = plans!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Plans { + String? id; + int? price; + int? oldPrice; + int? coins; + int? freeCoins; + String? title; + String? desc; + String? image; + + Plans( + {this.id, + this.price, + this.coins, + this.freeCoins, + this.title, + this.image, + this.desc, + this.oldPrice}); + + Plans.fromJson(Map json) { + id = json['id']; + price = json['price']; + coins = json['coins']; + image = json['image']; + freeCoins = json['free_coins']; + title = json['title']; + desc = json['description']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['price'] = price; + data['coins'] = coins; + data['free_coins'] = freeCoins; + data['title'] = title; + data['image'] = image; + data['description'] = desc; + return data; + } +} diff --git a/lib/data/model/popup_menu_model.dart b/lib/data/model/popup_menu_model.dart new file mode 100644 index 0000000..30ed613 --- /dev/null +++ b/lib/data/model/popup_menu_model.dart @@ -0,0 +1,9 @@ +import 'package:hoshan/core/gen/assets.gen.dart'; + +class PopupMenuModel { + final int id; + final String title; + final SvgGenImage? icon; + + PopupMenuModel({required this.id, required this.title, required this.icon}); +} diff --git a/lib/data/model/posts_model.dart b/lib/data/model/posts_model.dart new file mode 100644 index 0000000..24d3f46 --- /dev/null +++ b/lib/data/model/posts_model.dart @@ -0,0 +1,491 @@ +class PostsModel { + int? id; + String? date; + String? dateGmt; + Guid? guid; + String? modified; + String? modifiedGmt; + String? slug; + String? status; + String? type; + String? link; + Guid? title; + Content? content; + Content? excerpt; + int? author; + int? featuredMedia; + String? commentStatus; + String? pingStatus; + bool? sticky; + String? template; + String? format; + Meta? meta; + List? categories; + List? tags; + List? classList; + String? featuredImageSrc; + AuthorInfo? authorInfo; + Links? lLinks; + + PostsModel( + {this.id, + this.date, + this.dateGmt, + this.guid, + this.modified, + this.modifiedGmt, + this.slug, + this.status, + this.type, + this.link, + this.title, + this.content, + this.excerpt, + this.author, + this.featuredMedia, + this.commentStatus, + this.pingStatus, + this.sticky, + this.template, + this.format, + this.meta, + this.categories, + this.tags, + this.classList, + this.featuredImageSrc, + this.authorInfo, + this.lLinks}); + + PostsModel.fromJson(Map json) { + id = json['id']; + date = json['date']; + dateGmt = json['date_gmt']; + guid = json['guid'] != null ? Guid.fromJson(json['guid']) : null; + modified = json['modified']; + modifiedGmt = json['modified_gmt']; + slug = json['slug']; + status = json['status']; + type = json['type']; + link = json['link']; + title = json['title'] != null ? Guid.fromJson(json['title']) : null; + content = + json['content'] != null ? Content.fromJson(json['content']) : null; + excerpt = + json['excerpt'] != null ? Content.fromJson(json['excerpt']) : null; + author = json['author']; + featuredMedia = json['featured_media']; + commentStatus = json['comment_status']; + pingStatus = json['ping_status']; + sticky = json['sticky']; + template = json['template']; + format = json['format']; + meta = json['meta'] != null ? Meta.fromJson(json['meta']) : null; + categories = json['categories'].cast(); + tags = json['tags'].cast(); + classList = json['class_list'].cast(); + featuredImageSrc = json['featured_image_src']; + authorInfo = json['author_info'] != null + ? AuthorInfo.fromJson(json['author_info']) + : null; + lLinks = json['_links'] != null ? Links.fromJson(json['_links']) : null; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['date'] = date; + data['date_gmt'] = dateGmt; + if (guid != null) { + data['guid'] = guid!.toJson(); + } + data['modified'] = modified; + data['modified_gmt'] = modifiedGmt; + data['slug'] = slug; + data['status'] = status; + data['type'] = type; + data['link'] = link; + if (title != null) { + data['title'] = title!.toJson(); + } + if (content != null) { + data['content'] = content!.toJson(); + } + if (excerpt != null) { + data['excerpt'] = excerpt!.toJson(); + } + data['author'] = author; + data['featured_media'] = featuredMedia; + data['comment_status'] = commentStatus; + data['ping_status'] = pingStatus; + data['sticky'] = sticky; + data['template'] = template; + data['format'] = format; + if (meta != null) { + data['meta'] = meta!.toJson(); + } + data['categories'] = categories; + data['tags'] = tags; + data['class_list'] = classList; + data['featured_image_src'] = featuredImageSrc; + if (authorInfo != null) { + data['author_info'] = authorInfo!.toJson(); + } + if (lLinks != null) { + data['_links'] = lLinks!.toJson(); + } + return data; + } +} + +class Guid { + String? rendered; + + Guid({this.rendered}); + + Guid.fromJson(Map json) { + rendered = json['rendered']; + } + + Map toJson() { + final Map data = {}; + data['rendered'] = rendered; + return data; + } +} + +class Content { + String? rendered; + bool? protected; + + Content({this.rendered, this.protected}); + + Content.fromJson(Map json) { + rendered = json['rendered']; + protected = json['protected']; + } + + Map toJson() { + final Map data = {}; + data['rendered'] = rendered; + data['protected'] = protected; + return data; + } +} + +class Meta { + String? ubCttVia; + String? footnotes; + + Meta({this.ubCttVia, this.footnotes}); + + Meta.fromJson(Map json) { + ubCttVia = json['ub_ctt_via']; + footnotes = json['footnotes']; + } + + Map toJson() { + final Map data = {}; + data['ub_ctt_via'] = ubCttVia; + data['footnotes'] = footnotes; + return data; + } +} + +class AuthorInfo { + String? displayName; + String? authorLink; + + AuthorInfo({this.displayName, this.authorLink}); + + AuthorInfo.fromJson(Map json) { + displayName = json['display_name']; + authorLink = json['author_link']; + } + + Map toJson() { + final Map data = {}; + data['display_name'] = displayName; + data['author_link'] = authorLink; + return data; + } +} + +class Links { + List? self; + List? collection; + // List? about; + List? author; + // List? replies; + List? versionHistory; + List? predecessorVersion; + // List? wpAttachment; + List? wpTerm; + List? curies; + + Links( + {this.self, + this.collection, + // this.about, + this.author, + // this.replies, + this.versionHistory, + this.predecessorVersion, + // this.wpAttachment, + this.wpTerm, + this.curies}); + + Links.fromJson(Map json) { + if (json['self'] != null) { + self = []; + json['self'].forEach((v) { + self!.add(Self.fromJson(v)); + }); + } + if (json['collection'] != null) { + collection = []; + json['collection'].forEach((v) { + collection!.add(Collection.fromJson(v)); + }); + } + // if (json['about'] != null) { + // about = []; + // json['about'].forEach((v) { + // about!.add(About.fromJson(v)); + // }); + // } + if (json['author'] != null) { + author = []; + json['author'].forEach((v) { + author!.add(Author.fromJson(v)); + }); + } + // if (json['replies'] != null) { + // replies = []; + // json['replies'].forEach((v) { + // replies!.add(Replies.fromJson(v)); + // }); + // } + if (json['version-history'] != null) { + versionHistory = []; + json['version-history'].forEach((v) { + versionHistory!.add(VersionHistory.fromJson(v)); + }); + } + if (json['predecessor-version'] != null) { + predecessorVersion = []; + json['predecessor-version'].forEach((v) { + predecessorVersion!.add(PredecessorVersion.fromJson(v)); + }); + } + // if (json['wp:attachment'] != null) { + // wpAttachment = []; + // json['wp:attachment'].forEach((v) { + // wpAttachment!.add(WpAttachment.fromJson(v)); + // }); + // } + if (json['wp:term'] != null) { + wpTerm = []; + json['wp:term'].forEach((v) { + wpTerm!.add(WpTerm.fromJson(v)); + }); + } + if (json['curies'] != null) { + curies = []; + json['curies'].forEach((v) { + curies!.add(Curies.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + if (self != null) { + data['self'] = self!.map((v) => v.toJson()).toList(); + } + if (collection != null) { + data['collection'] = collection!.map((v) => v.toJson()).toList(); + } + // if (about != null) { + // data['about'] = about!.map((v) => v.toJson()).toList(); + // } + if (author != null) { + data['author'] = author!.map((v) => v.toJson()).toList(); + } + // if (replies != null) { + // data['replies'] = replies!.map((v) => v.toJson()).toList(); + // } + if (versionHistory != null) { + data['version-history'] = versionHistory!.map((v) => v.toJson()).toList(); + } + if (predecessorVersion != null) { + data['predecessor-version'] = + predecessorVersion!.map((v) => v.toJson()).toList(); + } + // if (wpAttachment != null) { + // data['wp:attachment'] = wpAttachment!.map((v) => v.toJson()).toList(); + // } + if (wpTerm != null) { + data['wp:term'] = wpTerm!.map((v) => v.toJson()).toList(); + } + if (curies != null) { + data['curies'] = curies!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Self { + String? href; + TargetHints? targetHints; + + Self({this.href, this.targetHints}); + + Self.fromJson(Map json) { + href = json['href']; + targetHints = json['targetHints'] != null + ? TargetHints.fromJson(json['targetHints']) + : null; + } + + Map toJson() { + final Map data = {}; + data['href'] = href; + if (targetHints != null) { + data['targetHints'] = targetHints!.toJson(); + } + return data; + } +} + +class TargetHints { + List? allow; + + TargetHints({this.allow}); + + TargetHints.fromJson(Map json) { + allow = json['allow'].cast(); + } + + Map toJson() { + final Map data = {}; + data['allow'] = allow; + return data; + } +} + +class Collection { + String? href; + + Collection({this.href}); + + Collection.fromJson(Map json) { + href = json['href']; + } + + Map toJson() { + final Map data = {}; + data['href'] = href; + return data; + } +} + +class Author { + bool? embeddable; + String? href; + + Author({this.embeddable, this.href}); + + Author.fromJson(Map json) { + embeddable = json['embeddable']; + href = json['href']; + } + + Map toJson() { + final Map data = {}; + data['embeddable'] = embeddable; + data['href'] = href; + return data; + } +} + +class VersionHistory { + int? count; + String? href; + + VersionHistory({this.count, this.href}); + + VersionHistory.fromJson(Map json) { + count = json['count']; + href = json['href']; + } + + Map toJson() { + final Map data = {}; + data['count'] = count; + data['href'] = href; + return data; + } +} + +class PredecessorVersion { + int? id; + String? href; + + PredecessorVersion({this.id, this.href}); + + PredecessorVersion.fromJson(Map json) { + id = json['id']; + href = json['href']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['href'] = href; + return data; + } +} + +class WpTerm { + String? taxonomy; + bool? embeddable; + String? href; + + WpTerm({this.taxonomy, this.embeddable, this.href}); + + WpTerm.fromJson(Map json) { + taxonomy = json['taxonomy']; + embeddable = json['embeddable']; + href = json['href']; + } + + Map toJson() { + final Map data = {}; + data['taxonomy'] = taxonomy; + data['embeddable'] = embeddable; + data['href'] = href; + return data; + } +} + +class Curies { + String? name; + String? href; + bool? templated; + + Curies({this.name, this.href, this.templated}); + + Curies.fromJson(Map json) { + name = json['name']; + href = json['href']; + templated = json['templated']; + } + + Map toJson() { + final Map data = {}; + data['name'] = name; + data['href'] = href; + data['templated'] = templated; + return data; + } +} diff --git a/lib/data/model/products_model.dart b/lib/data/model/products_model.dart new file mode 100644 index 0000000..289d62c --- /dev/null +++ b/lib/data/model/products_model.dart @@ -0,0 +1,14 @@ +import 'package:hoshan/core/gen/assets.gen.dart'; + +class ProductsModel { + final String title; + final String description; + final AssetGenImage image; + final String? link; + + ProductsModel( + {required this.title, + required this.description, + required this.image, + this.link}); +} diff --git a/lib/data/model/purchase_args.dart b/lib/data/model/purchase_args.dart new file mode 100644 index 0000000..64795b6 --- /dev/null +++ b/lib/data/model/purchase_args.dart @@ -0,0 +1,8 @@ +class PurchaseArgs { + final String message; + final bool success; + final int credit; + + PurchaseArgs( + {required this.message, required this.success, required this.credit}); +} diff --git a/lib/data/model/purchase_cards_models.dart b/lib/data/model/purchase_cards_models.dart new file mode 100644 index 0000000..2595ae1 --- /dev/null +++ b/lib/data/model/purchase_cards_models.dart @@ -0,0 +1,21 @@ +class OptionsModel { + final String text; + final bool active; + + OptionsModel({required this.text, this.active = false}); +} + +class PurchaseSelectModel { + final String text; + final bool active; + final bool isEconomic; + final int cost; + final int? oldCost; + + PurchaseSelectModel( + {required this.text, + this.active = false, + this.isEconomic = false, + required this.cost, + this.oldCost}); +} diff --git a/lib/data/model/report_model.dart b/lib/data/model/report_model.dart new file mode 100644 index 0000000..8001307 --- /dev/null +++ b/lib/data/model/report_model.dart @@ -0,0 +1,44 @@ +class ReportModel { + List? report; + + ReportModel({this.report}); + + ReportModel.fromJson(Map json) { + if (json['report'] != null) { + report = []; + json['report'].forEach((v) { + report!.add(Report.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + if (report != null) { + data['report'] = report!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Report { + String? date; + int? messagesCount; + int? coinUsage; + + Report({this.date, this.messagesCount, this.coinUsage}); + + Report.fromJson(Map json) { + date = json['date'] ?? json['hour'] ?? json['bot_name']; + messagesCount = json['messages_count']; + coinUsage = json['coin_usage']; + } + + Map toJson() { + final Map data = {}; + data['date'] = date; + data['messages_count'] = messagesCount; + data['coin_usage'] = coinUsage; + return data; + } +} diff --git a/lib/data/model/sort_by_model.dart b/lib/data/model/sort_by_model.dart new file mode 100644 index 0000000..fa25f2f --- /dev/null +++ b/lib/data/model/sort_by_model.dart @@ -0,0 +1,6 @@ +class SortByModel { + final String text; + final String value; + + SortByModel({required this.text, required this.value}); +} diff --git a/lib/data/model/ticket_model.dart b/lib/data/model/ticket_model.dart new file mode 100644 index 0000000..da2c3f7 --- /dev/null +++ b/lib/data/model/ticket_model.dart @@ -0,0 +1,81 @@ +import 'package:cross_file/cross_file.dart'; + +class TicketModel { + List tickets = []; + TicketModel(); + + TicketModel.fromJson(Map json) { + if (json['tickets'] != null) { + Map t = json['tickets']; + final keysIterable = t.keys; + List keysList = keysIterable.toList(); + for (var key in keysList) { + Map map = {'tickets': t[key], 'date': key}; + tickets.add(Tickets.fromJson(map)); + } + } + } +} + +class Tickets { + String? date; + List tickets = []; + + Tickets({List? tickets, this.date}) { + if (tickets != null) { + this.tickets.addAll(tickets); + } + } + + Tickets.fromJson(Map json) { + if (json['tickets'] != null) { + json['tickets'].forEach((v) { + tickets.add(Ticket.fromJson(v)); + }); + } + date = json['date']; + } + + Map toJson() { + final Map data = {}; + if (tickets.isNotEmpty) { + data['tickets'] = tickets.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Ticket { + int? id; + String? text; + String? role; + String? file; + XFile? localFile; + String? createdAt; + + Ticket( + {this.id, + this.text, + this.role, + this.file, + this.createdAt, + this.localFile}); + + Ticket.fromJson(Map json) { + id = json['id']; + text = json['text']; + role = json['role']; + file = json['file']; + createdAt = json['created_at']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['text'] = text; + data['role'] = role; + data['file'] = file; + data['created_at'] = createdAt; + return data; + } +} diff --git a/lib/data/model/tools_categories_model.dart b/lib/data/model/tools_categories_model.dart new file mode 100644 index 0000000..0989837 --- /dev/null +++ b/lib/data/model/tools_categories_model.dart @@ -0,0 +1,60 @@ +import 'package:hoshan/data/model/ai/bots_model.dart'; + +class ToolsCategoriesModel { + List? categories; + + ToolsCategoriesModel({this.categories}); + + ToolsCategoriesModel.fromJson(Map json) { + if (json['categories'] != null) { + categories = []; + json['categories'].forEach((v) { + categories!.add(Categories.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + if (categories != null) { + data['categories'] = categories!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Categories { + int? id; + String? name; + String? image; + String? description; + List? bots; + + Categories({this.id, this.name}); + + Categories.fromJson(Map json) { + id = json['id']; + name = json['name']; + image = json['image']; + description = json['description']; + + if (json['bots'] != null) { + bots = []; + json['bots'].forEach((v) { + bots!.add(Bots.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['name'] = name; + data['image'] = image; + data['description'] = description; + if (bots != null) { + data['bots'] = bots!.map((v) => v.toJson()).toList(); + } + return data; + } +} diff --git a/lib/data/repository/auth_repository.dart b/lib/data/repository/auth_repository.dart new file mode 100644 index 0000000..85974e9 --- /dev/null +++ b/lib/data/repository/auth_repository.dart @@ -0,0 +1,263 @@ +import 'package:cross_file/cross_file.dart'; +import 'package:dio/dio.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/core/utils/strings.dart'; +import 'package:hoshan/data/model/auth/login_model.dart'; +import 'package:hoshan/data/model/auth/user_info_model.dart'; +import 'package:hoshan/data/model/report_model.dart'; +import 'package:shamsi_date/shamsi_date.dart'; + +class AuthRepository { + static final DioService _dioService = DioService(); + + static Future getUserInfo() async { + try { + Response response = + await _dioService.sendRequest().get(DioService.getInfo); + return UserInfoModel.fromJson(response.data); + } catch (ex) { + rethrow; + } + } + + // static Future registerUser(String number) async { + // try { + // Response response = await _dioService.sendRequest().post( + // DioService.register, + // data: {'mobile_number': number.convertToEnglishNumber()}); + // return response; + // } catch (ex) { + // rethrow; + // } + // } + + static Future sendOtp(String number) async { + try { + Response response = await _dioService.sendRequest().post( + DioService.sendOTP, + data: {'mobile_number': number.convertToEnglishNumber()}); + return response.data['is_new']; + } catch (ex) { + rethrow; + } + } + + static Future loginWithPassword( + String number, String password) async { + try { + Response response = await _dioService.sendRequest().post( + DioService.loginWithPassword, + data: {'username': number, 'password': password}); + return LoginModel.fromJson(response.data); + } catch (ex) { + rethrow; + } + } + + static Future loginWithOTP(String number, String otp) async { + try { + Response response = await _dioService.sendRequest().post( + DioService.loginWithOTP, + data: {"mobile_number": number.convertToEnglishNumber(), "otp": otp}); + return LoginModel.fromJson(response.data); + } catch (ex) { + rethrow; + } + } + + static Future checkUsernameIsValid(String username) async { + try { + Response response = await _dioService + .sendRequest() + .post(DioService.checkUsername, data: {"username": username}); + return response.data['available']; + } catch (ex) { + rethrow; + } + } + + static Future editUsername(String username) async { + try { + Response response = await _dioService + .sendRequest() + .put(DioService.editUsername, data: {"username": username}); + return (response.statusCode!) >= 200 && (response.statusCode!) < 300; + } catch (ex) { + return false; + } + } + + static Future editImageProfile(XFile image) async { + try { + FormData formData = FormData(); + + MultipartFile multipartFile = await DioService.getMultipartFile(image); + + formData.files.add(MapEntry('file', multipartFile)); + Response response = await _dioService + .sendRequest() + .put(DioService.editProfile, data: formData); + return (response.statusCode!) >= 200 && (response.statusCode!) < 300; + } catch (ex) { + return false; + } + } + + static Future editPasswordProfile(String password) async { + try { + Response response = await _dioService + .sendRequest() + .put(DioService.editPassword, data: {"password": password}); + return (response.statusCode!) >= 200 && (response.statusCode!) < 300; + } catch (ex) { + return false; + } + } + + static Future editCardNumber(String? cardNumber) async { + try { + Response response = await _dioService + .sendRequest() + .put(DioService.cardNumber, data: {"card_number": cardNumber}); + return (response.statusCode!) >= 200 && (response.statusCode!) < 300; + } catch (ex) { + return false; + } + } + + static Future checkGiftCode(String code) async { + try { + Response response = await _dioService + .sendRequest() + .post(DioService.giftCode, data: {"code": code}); + return response.data['detail']; + } catch (ex) { + rethrow; + } + } + + static Future getReport({required Jalali? startDate}) async { + try { + startDate ??= Jalali.now(); + + final shamsiYear = startDate.year; + final shamsiMounth = startDate.month; + + startDate = Jalali(shamsiYear, shamsiMounth, 1); + Jalali endDate = Jalali( + shamsiYear, + shamsiMounth + 1 > 12 ? 12 : shamsiMounth + 1, + shamsiMounth + 1 > 12 ? 30 : 1) + .addDays(-1); + Gregorian sMiladiDate = startDate.toGregorian(); + Gregorian eMiladiDate = endDate.toGregorian(); + + String sDate = + '${sMiladiDate.year}-${sMiladiDate.month.toString().padLeft(2, '0')}-${sMiladiDate.day.toString().padLeft(2, '0')}'; + String eDate = + '${eMiladiDate.year}-${eMiladiDate.month.toString().padLeft(2, '0')}-${eMiladiDate.day.toString().padLeft(2, '0')}'; + Response response = await _dioService.sendRequest().get( + DioService.reportPeriodic, + queryParameters: {'start_date': sDate, 'end_date': eDate}); + return ReportModel.fromJson(response.data); + } catch (ex) { + rethrow; + } + } + + static Future getReportCoin({required Jalali? startDate}) async { + try { + startDate ??= Jalali.now(); + + final shamsiYear = startDate.year; + final shamsiMounth = startDate.month; + + startDate = Jalali(shamsiYear, shamsiMounth, 1); + Jalali endDate = Jalali( + shamsiYear, + shamsiMounth + 1 > 12 ? 12 : shamsiMounth + 1, + shamsiMounth + 1 > 12 ? 30 : 1) + .addDays(-1); + + Gregorian sMiladiDate = startDate.toGregorian(); + Gregorian eMiladiDate = endDate.toGregorian(); + + String sDate = + '${sMiladiDate.year}-${sMiladiDate.month.toString().padLeft(2, '0')}-${sMiladiDate.day.toString().padLeft(2, '0')}'; + String eDate = + '${eMiladiDate.year}-${eMiladiDate.month.toString().padLeft(2, '0')}-${eMiladiDate.day.toString().padLeft(2, '0')}'; + Response response = await _dioService.sendRequest().get( + DioService.reportPiCoin, + queryParameters: {'start_date': sDate, 'end_date': eDate}); + return ReportModel.fromJson(response.data); + } catch (ex) { + rethrow; + } + } + + static Future setFCMtoken(String token) async { + try { + Response response = await _dioService + .sendRequest() + .put(DioService.fCMtoken, data: {"token": token}); + return ReportModel.fromJson(response.data); + } catch (ex) { + rethrow; + } + } + + static Future getRemaining() async { + try { + Response response = + await _dioService.sendRequest().get(DioService.remaining); + return response; + } catch (ex) { + rethrow; + } + } + + static Future addSubUser(String mobileNumber, int level) async { + try { + final number = mobileNumber.convertToEnglishNumber(); + + Response response = await _dioService.sendRequest().post( + DioService.addSubUser, + data: { + "mobile_number": number, + "level": level, + }, + ); + return (response.statusCode!) >= 200 && (response.statusCode!) < 300; + } catch (ex) { + rethrow; + } + } + + static Future deleteSubUser(String id) async { + try { + final response = + await _dioService.sendRequest().delete(DioService.deleteSubUser(id)); + return (response.statusCode ?? 500) >= 200 && + (response.statusCode ?? 0) < 300; + } catch (ex) { + rethrow; + } + } + + static Future>> getSubUsers() async { + try { + Response response = await _dioService.sendRequest().get(DioService.getSubUsers); + + // استخراج لیست کاربران از پاسخ جیسون + // ساختار: { "sub_users": [...], "total_count": 1 } + List data = response.data['sub_users']; + + // تبدیل به لیستی از Mapها + return List>.from(data); + } catch (ex) { + rethrow; + } + } +} + + diff --git a/lib/data/repository/bot_repository.dart b/lib/data/repository/bot_repository.dart new file mode 100644 index 0000000..d87e71b --- /dev/null +++ b/lib/data/repository/bot_repository.dart @@ -0,0 +1,247 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/data/model/assistant_comments_model.dart'; +import 'package:hoshan/data/model/assistant_personal_info_model.dart'; +import 'package:hoshan/data/model/create_assistant_request_model.dart'; +import 'package:hoshan/data/model/effects_model.dart'; +import 'package:hoshan/data/model/event_model.dart'; +import 'package:hoshan/data/model/global_assistant_bots_model.dart'; +import 'package:hoshan/data/model/media_model.dart'; +import 'package:hoshan/data/model/personal_assistants_bots.dart'; +import 'package:hoshan/data/model/photo_gen_model.dart'; +import 'package:hoshan/data/model/tools_categories_model.dart'; + +class BotRepository { + static final DioService _dioService = DioService(); + + static Future getBots({final String? search}) async { + try { + Map? queryParameters = {}; + if (search != null) { + queryParameters['query'] = search; + } + Response response = await _dioService + .sendRequest() + .get(DioService.getAllBots, queryParameters: queryParameters); + return BotsModel.fromJson(response.data); + } catch (ex) { + rethrow; + } + } + + static Future getSingleBot({required final int id}) async { + try { + Response response = + await _dioService.sendRequest().get(DioService.getSingleBot(id)); + return Bots.fromJson(response.data['bot']); + } catch (ex) { + rethrow; + } + } + + static Future getGlobalAssistant( + {final bool? marked, final int? categorieId}) async { + try { + Map? queryParameters = {}; + if (categorieId != null) { + queryParameters['category'] = categorieId; + } + if (marked != null) { + queryParameters['marked'] = marked; + } + + Response response = await _dioService.sendRequest().get( + DioService.getGlobalAssistants, + queryParameters: queryParameters); + return GlobalAssistantBotsModel.fromJson(response.data); + } catch (ex) { + rethrow; + } + } + + static Future getPersonalAssistant() async { + try { + Response response = + await _dioService.sendRequest().get(DioService.getPersonalAssistants); + return PersonalAssistantBotsModel.fromJson(response.data); + } catch (ex) { + rethrow; + } + } + + static Future getAllCategories() async { + try { + Response response = + await _dioService.sendRequest().get(DioService.allCategories); + return ToolsCategoriesModel.fromJson(response.data); + } catch (ex) { + rethrow; + } + } + + static Future getToolsCategories() async { + try { + Response response = + await _dioService.sendRequest().get(DioService.toolsCategories); + return ToolsCategoriesModel.fromJson(response.data); + } catch (ex) { + rethrow; + } + } + + static Future botMark( + {required final int id, required final bool marked}) async { + try { + Response response = await _dioService + .sendRequest() + .put(DioService.markedBot(id), data: {'marked': marked}); + return response; + } catch (ex) { + rethrow; + } + } + + static Future createBot( + {required final CreateAssistantRequestModel model}) async { + try { + FormData formDatBody = await model.toFormData(); + final response = await _dioService.sendRequest().post( + DioService.getAllBots, + data: formDatBody, + options: Options(sendTimeout: 5.minutes)); + return response; + } catch (e) { + rethrow; + } + } + + static Future editBot( + {required final int id, + required final CreateAssistantRequestModel model}) async { + try { + FormData formDatBody = await model.toFormData(); + final response = await _dioService.sendRequest().put( + DioService.getPersonalAssistant(id), + data: formDatBody, + options: Options(sendTimeout: 5.minutes)); + return response; + } catch (e) { + rethrow; + } + } + + static Future deleteBot({required final int id}) async { + try { + final response = await _dioService + .sendRequest() + .delete(DioService.getPersonalAssistant(id)); + return response; + } catch (e) { + rethrow; + } + } + + static Future getAssistantGlobalInfo(int id) async { + try { + final response = await _dioService + .sendRequest() + .get(DioService.getGlobalAssistant(id)); + return Bots.fromJson(response.data['bot']); + } catch (e) { + rethrow; + } + } + + static Future getAssistantPersonalInfo( + int id) async { + try { + final response = await _dioService + .sendRequest() + .get(DioService.getPersonalAssistant(id)); + return AssistantPersonalInfoModel.fromJson(response.data); + } catch (e) { + rethrow; + } + } + + static Future getAssistantComments( + int id, int page) async { + try { + final response = await _dioService.sendRequest().get( + DioService.getAssistantComments(id), + queryParameters: {'page': page}); + return AssistantCommentsModel.fromJson(response.data); + } catch (e) { + rethrow; + } + } + + static Future addCommentInAssistant( + int id, AssistantComments comment) async { + try { + final response = await _dioService.sendRequest().put( + DioService.getAssistantComments(id), + data: {'text': comment.text, 'score': comment.score}); + return response; + } catch (e) { + rethrow; + } + } + + static Future getMedias() async { + try { + final response = await _dioService.sendRequest().get( + DioService.media, + ); + return MediasModel.fromJson(response.data); + } catch (e) { + rethrow; + } + } + + static Future getSingleMedia(int id) async { + try { + final response = await _dioService.sendRequest().get( + DioService.singleMedia(id), + ); + return GensModel.fromJson(response.data); + } catch (e) { + rethrow; + } + } + + static Future getAllEffects() async { + try { + final response = await _dioService.sendRequest().get( + DioService.effects, + ); + return EffectsModel.fromJson(response.data); + } catch (e) { + rethrow; + } + } + + static Future getAllCharachters() async { + try { + final response = await _dioService.sendRequest().get( + DioService.characters, + ); + return GensModel.fromJson(response.data); + } catch (e) { + rethrow; + } + } + + static Future getAllEvents() async { + try { + final response = await _dioService.sendRequest().get( + DioService.events, + ); + return EventModel.fromJson(response.data); + } catch (e) { + rethrow; + } + } +} diff --git a/lib/data/repository/chatbot_repository.dart b/lib/data/repository/chatbot_repository.dart new file mode 100644 index 0000000..1900561 --- /dev/null +++ b/lib/data/repository/chatbot_repository.dart @@ -0,0 +1,296 @@ +import 'dart:convert'; + +import 'package:cross_file/cross_file.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/core/utils/file.dart'; +import 'package:hoshan/data/model/ai/chats_history_model.dart'; +import 'package:hoshan/data/model/ai/messages_model.dart'; +import 'package:hoshan/data/model/ai/related_questions_model.dart'; +import 'package:hoshan/data/model/ai/send_message_model.dart'; +import 'package:hoshan/data/model/banner_model.dart'; + +class ChatbotRepository { + static final DioService _dioService = DioService(); + static CancelToken? cancelToken; + + static void cancelSendMessage() { + cancelToken?.cancel(); + } + + static Stream sendMessage(SendMessageModel sendMessageModel) async* { + cancelToken = CancelToken(); + + try { + FormData formDatBody = FormData(); + + formDatBody.fields + .add(MapEntry('bot_id', sendMessageModel.botId.toString())); + formDatBody.fields.add(MapEntry( + 'retry', (sendMessageModel.retry ?? false).toString().toLowerCase())); + formDatBody.fields.add(MapEntry( + 'ghost', (sendMessageModel.ghost ?? false).toString().toLowerCase())); + if (sendMessageModel.query != null) { + formDatBody.fields + .add(MapEntry('query', sendMessageModel.query.toString())); + } + if (sendMessageModel.id != null) { + formDatBody.fields.add(MapEntry("id", sendMessageModel.id.toString())); + } + + if (sendMessageModel.file != null) { + MultipartFile multipartFile = + await DioService.getMultipartFile(sendMessageModel.file!); + String nameFileField = sendMessageModel.file!.isImage() + ? 'image' + : sendMessageModel.file!.isAudio() + ? 'audio' + : 'doc'; + formDatBody.files.add(MapEntry(nameFileField, multipartFile)); + } + Response response = await _dioService + .sendRequestStream() + .post(DioService.sendMessage, + data: formDatBody, cancelToken: cancelToken, options: Options()); + await for (var value in response.data!.stream) { + if (kDebugMode) { + print(utf8.decode(value)); + } + yield utf8.decode(value) + // .replaceAll('}{', ' } \n { ') + // .replaceAll('}', ' } ') + // .replaceAll('{', ' { ') + ; + } + } catch (e) { + rethrow; + } + } + + static Stream sendMessageTool( + SendMessageModel sendMessageModel) async* { + cancelToken = CancelToken(); + + try { + FormData formDatBody = FormData(); + + formDatBody.fields.add(MapEntry( + 'retry', (sendMessageModel.retry ?? false).toString().toLowerCase())); + + formDatBody.fields.add(MapEntry( + 'ghost', (sendMessageModel.ghost ?? false).toString().toLowerCase())); + if (sendMessageModel.query != null) { + formDatBody.fields + .add(MapEntry('query', sendMessageModel.query.toString())); + } + if (sendMessageModel.option != null) { + formDatBody.fields + .add(MapEntry('options', sendMessageModel.option.toString())); + } + if (sendMessageModel.id != null) { + formDatBody.fields.add(MapEntry("id", sendMessageModel.id.toString())); + } + + if (sendMessageModel.file != null) { + MultipartFile multipartFile = + await DioService.getMultipartFile(sendMessageModel.file!); + String nameFileField = sendMessageModel.file!.isImage() + ? 'image' + : sendMessageModel.file!.isAudio() + ? 'audio' + : 'doc'; + formDatBody.files.add(MapEntry(nameFileField, multipartFile)); + } + + Response response = + await _dioService.sendRequestStream().post( + DioService.sendMessageTool(id: sendMessageModel.botId!), + data: formDatBody, + cancelToken: cancelToken, + ); + await for (var value in response.data!.stream) { + if (kDebugMode) { + print(utf8.decode(value)); + } + yield utf8.decode(value) + // .replaceAll('}{', ' } \n { ') + // .replaceAll('}', ' } ') + // .replaceAll('{', ' { ') + ; + } + } catch (e) { + rethrow; + } + } + + static Future getChats( + {final bool archive = false, + final String? search, + final String? date, + required final String? type, + required final int page}) async { + try { + Map queryParameters = { + 'archive': archive.toString().toLowerCase(), + 'page': page, + 'type': type, + }; + if (search != null) { + queryParameters.addAll({'query': search}); + } + + if (date != null) { + final sp = date.split('-'); + final d = '${sp[0]}-${sp[1].padLeft(2, '0')}-${sp[2].padLeft(2, '0')}'; + queryParameters.addAll({'date': d}); + } + + Response response = await _dioService + .sendRequest() + .get(DioService.sendMessage, queryParameters: queryParameters); + return ChatsHistoryModel.fromJson(response.data); + } catch (ex) { + rethrow; + } + } + + static Future getMessages({required final int id}) async { + try { + Response response = await _dioService.sendRequest().get( + DioService.chatHistory(id: id), + ); + return MessagesModel.fromJson(response.data); + } catch (ex) { + rethrow; + } + } + + static Future editChat( + {required final int id, required final String title}) async { + try { + final response = await _dioService + .sendRequest() + .put(DioService.editTitle(id: id), data: {"title": title}); + return response; + } catch (ex) { + rethrow; + } + } + + static Future deleteChat({required final int id}) async { + try { + final response = await _dioService + .sendRequest() + .delete(DioService.chatHistory(id: id)); + return response; + } catch (ex) { + rethrow; + } + } + + static Future deleteAllChats({required final bool archive}) async { + try { + final response = await _dioService + .sendRequest() + .delete(DioService.sendMessage, data: {"archive": archive}); + return response; + } catch (ex) { + rethrow; + } + } + + static Future likedMessage( + {required final int chatId, + required final String messageId, + required final bool? like}) async { + try { + final response = await _dioService.sendRequest().put( + DioService.likeMessage(id: chatId, messageId: messageId), + data: {"like": like}); + return response; + } catch (ex) { + rethrow; + } + } + + static Future deleteMessage( + {required final int chatId, required final String messageId}) async { + try { + final response = await _dioService.sendRequest().delete( + DioService.messageDelete(id: chatId, messageId: messageId), + ); + return response; + } catch (ex) { + rethrow; + } + } + + static Future getRelatedQuestions( + {required final int chatId, + required final String messageId, + required final String content}) async { + try { + final response = await _dioService.sendRequest().post( + DioService.relatedQuestions( + id: chatId, + ), + data: {"id": messageId, "content": content}); + return RelatedQuestionsModel.fromJson(response.data); + } catch (ex) { + rethrow; + } + } + + static Future archiveChat(final int chatId, final bool archive) async { + try { + final response = await _dioService.sendRequest().put( + DioService.archive( + id: chatId, + ), + data: {"archive": archive}); + return (response.statusCode!) >= 200 && (response.statusCode!) < 300; + } catch (ex) { + rethrow; + } + } + + static Future createXFileFromUrl(String url) async { + try { + final response = await _dioService.sendRequest().get( + url, + options: Options(responseType: ResponseType.bytes), + ); + if (response.statusCode == 200) { + // Create an XFile from the bytes + Uint8List bytes = response.data; // Get the bytes from the response + return XFile.fromData(bytes, + name: + 'file_${DateTime.now().millisecondsSinceEpoch}.txt'); // Customize the name and extension as needed + } else { + throw Exception('Failed to load file from URL'); + } + } catch (e) { + if (kDebugMode) { + print('Error fetching file: $e'); + } + return null; // Return null in case of an error + } + } + + static Future> getBanners() async { + try { + final response = await _dioService.sendRequest().get( + DioService.homeBanner, + ); + final banners = []; + response.data['banners'].forEach((v) { + banners.add(Banners.fromJson(v)); + }); + + return banners; + } catch (e) { + rethrow; // Return null in case of an error + } + } +} diff --git a/lib/data/repository/forum_repository.dart b/lib/data/repository/forum_repository.dart new file mode 100644 index 0000000..764631a --- /dev/null +++ b/lib/data/repository/forum_repository.dart @@ -0,0 +1,90 @@ +import 'package:cross_file/cross_file.dart'; +import 'package:dio/dio.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/data/model/forum_model.dart'; + +class ForumRepository { + static final DioService _dioService = DioService(); + + static Future getForumComments( + {required int categoryId, int? page, String? orderBy}) async { + try { + Map? queryParameters = {'category_id': categoryId}; + if (page != null) { + queryParameters['page'] = page; + } + if (orderBy != null) { + queryParameters['order_by'] = orderBy; + } + Response response = await _dioService + .sendRequest() + .get(DioService.forumComments, queryParameters: queryParameters); + return ForumModel.fromJson(response.data); + } catch (ex) { + rethrow; + } + } + + static Future getForumCommentsReplies( + {required int categoryId, required int id, int? page}) async { + try { + Map? queryParameters = {'category_id': categoryId}; + if (page != null) { + queryParameters['page'] = page; + } + Response response = await _dioService.sendRequest().get( + DioService.forumReplieComments(id), + queryParameters: queryParameters); + return ForumModel.fromJson(response.data); + } catch (ex) { + rethrow; + } + } + + static Future sendForum({ + required String text, + required int categoryId, + XFile? image, + int? parentId, + String? repliedUserId, + }) async { + try { + FormData formDatBody = FormData(); + + formDatBody.fields.add(MapEntry('text', text)); + formDatBody.fields.add(MapEntry('category_id', categoryId.toString())); + if (parentId != null) { + formDatBody.fields.add(MapEntry('parent_id', parentId.toString())); + } + if (repliedUserId != null) { + formDatBody.fields + .add(MapEntry('replied_user_id', repliedUserId.toString())); + } + + if (image != null) { + MultipartFile multipartFile = await DioService.getMultipartFile(image); + String nameFileField = 'image'; + formDatBody.files.add(MapEntry(nameFileField, multipartFile)); + } + + Response response = await _dioService + .sendRequest() + .post(DioService.forumComments, data: formDatBody); + return Comment.fromJson(response.data['comment']); + } catch (ex) { + rethrow; + } + } + + static Future likedMessage( + {required final int id, required final int status}) async { + try { + final response = await _dioService + .sendRequest() + .put(DioService.forumCommentsFeedback(id), data: {"status": status}); + return response; + } catch (ex) { + rethrow; + } + } +} diff --git a/lib/data/repository/paymant_repository.dart b/lib/data/repository/paymant_repository.dart new file mode 100644 index 0000000..9aa7a82 --- /dev/null +++ b/lib/data/repository/paymant_repository.dart @@ -0,0 +1,66 @@ +import 'package:dio/dio.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/data/model/billings_history_model.dart'; +import 'package:hoshan/data/model/discount_model.dart'; +import 'package:hoshan/data/model/plans_model.dart'; + +class PaymantRepository { + static final DioService _dioService = DioService(); + + static Future getLinkPaymant(int amount, {final String? code}) async { + try { + Map? queryParameters = {'amount': amount}; + if (code != null && code.isNotEmpty) { + queryParameters.addAll({'code': code}); + } + Response response = await _dioService + .sendRequest() + .get(DioService.paymant, queryParameters: queryParameters); + return response.data['url']; + } catch (ex) { + rethrow; + } + } + + static Future> getPaymantHistory() async { + try { + Response response = + await _dioService.sendRequest().get(DioService.paymantHistory); + return BillingsHistoryModel.fromJson(response.data).billings!; + } catch (ex) { + rethrow; + } + } + + static Future> getPaymantPlans() async { + try { + Response response = + await _dioService.sendRequest().get(DioService.paymantPlans); + return PlansModel.fromJson(response.data).plans!; + } catch (ex) { + rethrow; + } + } + + static Future getDiscount(String code, String prId) async { + try { + Response response = await _dioService + .sendRequest() + .post(DioService.discount, data: {"code": code, 'product_id': prId}); + return DiscountModel.fromJson(response.data); + } catch (ex) { + rethrow; + } + } + + static Future setSettlement(bool isRial) async { + try { + Response response = await _dioService.sendRequest().post( + DioService.settlement, + data: {"type": isRial ? 'money' : 'coin'}); + return response.data['msg']; + } catch (ex) { + rethrow; + } + } +} diff --git a/lib/data/repository/ticket_repository.dart b/lib/data/repository/ticket_repository.dart new file mode 100644 index 0000000..0481c66 --- /dev/null +++ b/lib/data/repository/ticket_repository.dart @@ -0,0 +1,49 @@ +import 'package:cross_file/cross_file.dart'; +import 'package:dio/dio.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/data/model/ticket_model.dart'; + +class TicketRepository { + static final DioService _dioService = DioService(); + + static Future getTickets() async { + try { + Response response = + await _dioService.sendRequest().get(DioService.ticket); + return TicketModel.fromJson(response.data); + } catch (ex) { + rethrow; + } + } + + static Future deleteTickets(int id) async { + try { + Response response = + await _dioService.sendRequest().delete(DioService.deleteTicket(id)); + return response; + } catch (ex) { + rethrow; + } + } + + static Future sendTickets({required String text, XFile? file}) async { + try { + FormData formDatBody = FormData(); + + formDatBody.fields.add(MapEntry('text', text)); + + if (file != null) { + MultipartFile multipartFile = await DioService.getMultipartFile(file); + + formDatBody.files.add(MapEntry('file', multipartFile)); + } + + Response response = await _dioService + .sendRequest() + .post(DioService.ticket, data: formDatBody); + return Ticket.fromJson(response.data['ticket']); + } catch (ex) { + rethrow; + } + } +} diff --git a/lib/data/snackbar_messages_model.dart b/lib/data/snackbar_messages_model.dart new file mode 100644 index 0000000..dbabe9f --- /dev/null +++ b/lib/data/snackbar_messages_model.dart @@ -0,0 +1,8 @@ +import 'package:another_flushbar/flushbar.dart'; + +class SnackbarMessagesModel { + final String id; + final Flushbar flushbar; + + SnackbarMessagesModel({required this.id, required this.flushbar}); +} diff --git a/lib/data/storage/shared_preferences_helper.dart b/lib/data/storage/shared_preferences_helper.dart new file mode 100644 index 0000000..200c8dd --- /dev/null +++ b/lib/data/storage/shared_preferences_helper.dart @@ -0,0 +1,127 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class SharedPreferencesHelper { + static late SharedPreferences preferences; + + static Future initial() async { + preferences = await SharedPreferences.getInstance(); + } + + static const String token = 'auth_token'; + static const String themeMode = 'theme-mode'; + static const String inviteLastTime = 'invite_last_time'; + static const String videoGuid = 'video-guid'; + static const String audioGuid = 'audio-guid'; + static const String imageGuid = 'image-guid'; + static const String onboardingSeen = 'onboarding-seen'; +} + +class AuthTokenStorage { + static String getToken() { + final prefs = SharedPreferencesHelper.preferences; + return prefs.getString(SharedPreferencesHelper.token) ?? ""; + } + + static Future setToken(String token) async { + final prefs = SharedPreferencesHelper.preferences; + await prefs.setString(SharedPreferencesHelper.token, token); + } + + static void clearToken() { + final prefs = SharedPreferencesHelper.preferences; + prefs.remove(SharedPreferencesHelper.token); + } +} + +class ThemeModeStorage { + static String getMode() { + final prefs = SharedPreferencesHelper.preferences; + return prefs.getString(SharedPreferencesHelper.themeMode) ?? "system"; + } + + static void setMode(String mode) { + final prefs = SharedPreferencesHelper.preferences; + prefs.setString(SharedPreferencesHelper.themeMode, mode); + } + + static void clearMode() { + final prefs = SharedPreferencesHelper.preferences; + prefs.remove(SharedPreferencesHelper.themeMode); + } +} + +class OnBoardingStorage { + static bool hasSeen() { + final prefs = SharedPreferencesHelper.preferences; + return prefs.getBool(SharedPreferencesHelper.onboardingSeen) ?? false; + } + + static Future setAsSeen() async { + final prefs = SharedPreferencesHelper.preferences; + await prefs.setBool(SharedPreferencesHelper.onboardingSeen, true); + } +} + +class InvitePopupStorage { + static String getLastTime() { + final prefs = SharedPreferencesHelper.preferences; + return prefs.getString(SharedPreferencesHelper.inviteLastTime) ?? ""; + } + + static void setLastTime(String lastTimeIso) { + final prefs = SharedPreferencesHelper.preferences; + prefs.setString(SharedPreferencesHelper.inviteLastTime, lastTimeIso); + } + + static void clearLastTime() { + final prefs = SharedPreferencesHelper.preferences; + prefs.remove(SharedPreferencesHelper.inviteLastTime); + } +} + +class GuidsStorage { + static bool isSeenVideo() { + final prefs = SharedPreferencesHelper.preferences; + return prefs.getBool(SharedPreferencesHelper.videoGuid) ?? false; + } + + static void setSeenVideo(bool seen) { + final prefs = SharedPreferencesHelper.preferences; + prefs.setBool(SharedPreferencesHelper.videoGuid, seen); + } + + static void clearSeenVideo() { + final prefs = SharedPreferencesHelper.preferences; + prefs.remove(SharedPreferencesHelper.videoGuid); + } + + static bool isSeenAudio() { + final prefs = SharedPreferencesHelper.preferences; + return prefs.getBool(SharedPreferencesHelper.audioGuid) ?? false; + } + + static void setSeenAudio(bool seen) { + final prefs = SharedPreferencesHelper.preferences; + prefs.setBool(SharedPreferencesHelper.audioGuid, seen); + } + + static void clearSeenAudio() { + final prefs = SharedPreferencesHelper.preferences; + prefs.remove(SharedPreferencesHelper.audioGuid); + } + + static bool isSeenImage() { + final prefs = SharedPreferencesHelper.preferences; + return prefs.getBool(SharedPreferencesHelper.imageGuid) ?? false; + } + + static void setSeenImage(bool seen) { + final prefs = SharedPreferencesHelper.preferences; + prefs.setBool(SharedPreferencesHelper.imageGuid, seen); + } + + static void clearSeenImage() { + final prefs = SharedPreferencesHelper.preferences; + prefs.remove(SharedPreferencesHelper.imageGuid); + } +} diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..f0ead28 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,89 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + return macos; + case TargetPlatform.windows: + return windows; + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyDL9KGCMFdbTeGbtXZ9-AnBpUPRY7wGp4k', + appId: '1:581103504002:web:8facd97674b83ac218829b', + messagingSenderId: '581103504002', + projectId: 'hoshan-42d9f', + authDomain: 'hoshan-42d9f.firebaseapp.com', + storageBucket: 'hoshan-42d9f.appspot.com', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyDi2WRiOSEws1alpLitxX0zsX14rT71aPk', + appId: '1:581103504002:android:d12a150d3d54570418829b', + messagingSenderId: '581103504002', + projectId: 'hoshan-42d9f', + storageBucket: 'hoshan-42d9f.appspot.com', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyAENpjFJ2VeVQEt3_Iud9gfqLvuffb7EUI', + appId: '1:581103504002:ios:4e51253f750a0fc818829b', + messagingSenderId: '581103504002', + projectId: 'hoshan-42d9f', + storageBucket: 'hoshan-42d9f.appspot.com', + iosClientId: '581103504002-2e454pgr7fes6b94ptbt82rpfldehedq.apps.googleusercontent.com', + iosBundleId: 'com.houshan.hoshan', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyAENpjFJ2VeVQEt3_Iud9gfqLvuffb7EUI', + appId: '1:581103504002:ios:4e51253f750a0fc818829b', + messagingSenderId: '581103504002', + projectId: 'hoshan-42d9f', + storageBucket: 'hoshan-42d9f.appspot.com', + iosClientId: '581103504002-2e454pgr7fes6b94ptbt82rpfldehedq.apps.googleusercontent.com', + iosBundleId: 'com.houshan.hoshan', + ); + + static const FirebaseOptions windows = FirebaseOptions( + apiKey: 'AIzaSyDL9KGCMFdbTeGbtXZ9-AnBpUPRY7wGp4k', + appId: '1:581103504002:web:084a33707f94511118829b', + messagingSenderId: '581103504002', + projectId: 'hoshan-42d9f', + authDomain: 'hoshan-42d9f.firebaseapp.com', + storageBucket: 'hoshan-42d9f.appspot.com', + ); + +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..0793dac --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,202 @@ +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_downloader/flutter_downloader.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/core/services/ad/cubit/on_show_add_cubit.dart'; +import 'package:hoshan/core/services/ad/tapsell_service.dart'; +import 'package:hoshan/core/services/firebase/firebase_api.dart'; +import 'package:hoshan/data/storage/shared_preferences_helper.dart'; +import 'package:hoshan/firebase_options.dart'; +import 'package:hoshan/ui/screens/cmp/cubit/cmp_cubit.dart'; +import 'package:hoshan/ui/screens/gmedia/cubit/medias_cubit.dart'; +import 'package:hoshan/ui/screens/gmedia/cubit/single_media_cubit.dart'; +import 'package:hoshan/ui/screens/main/assistant/bloc/global_assistants_bloc.dart'; +import 'package:hoshan/ui/screens/main/assistant/bloc/personal_assistants_bloc.dart'; +import 'package:hoshan/ui/screens/main/home/bloc/bots_bloc.dart'; +import 'package:hoshan/ui/screens/main/home/bloc/cubit/posts_cubit.dart'; +import 'package:hoshan/ui/screens/main/forum/cubit/category_cubit.dart'; +import 'package:hoshan/ui/screens/library/bloc/chats_history_bloc.dart'; +import 'package:hoshan/ui/screens/main/home/cubit/main_chat_bot_cubit.dart'; +import 'package:hoshan/ui/screens/main/persons/cubit/persons_cubit.dart'; +import 'package:hoshan/ui/screens/setting/bloc/paymant_history_bloc.dart'; +import 'package:hoshan/ui/screens/setting/cubit/ad_remaining_cubit.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/screens/tools/bloc/tools_bloc.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/my_custom_scroll_behavior.dart'; +import 'package:hoshan/ui/theme/theme.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:url_strategy/url_strategy.dart'; + +@pragma('vm:entry-point') +Future _onBackgroundMessage(RemoteMessage message) async {} + +void main() async { + setPathUrlStrategy(); + WidgetsFlutterBinding.ensureInitialized(); + + // 1. Wrap SharedPreferences in try-catch + try { + await SharedPreferencesHelper.initial(); + } catch (e) { + debugPrint("Error initializing SharedPreferences: $e"); + } + + // 2. Wrap Tapsell in try-catch (High probability of crash here) + try { + await TapsellService.initialize(); + } catch (e) { + debugPrint("Error initializing Tapsell: $e"); + } + + // 3. Firebase Initialization + try { + FirebaseMessaging.onBackgroundMessage(_onBackgroundMessage); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform); + FirebasApi.initialNotifications(); + } catch (e) { + debugPrint('Error initializing Firebase: $e'); + } + + // 4. Downloader Initialization + try { + if (!kIsWeb) { + await FlutterDownloader.initialize(debug: true, ignoreSsl: true); + } + } catch (e) { + debugPrint('Error initializing FlutterDownloader: $e'); + } + + // Run App + if (kReleaseMode) { + await SentryFlutter.init((options) { + options.dsn = + 'https://9bb2d441e4aa30034c88783673cd64a4@o4508585857384448.ingest.de.sentry.io/4509043371212880'; + + options.sendDefaultPii = true; + }, + // Init your App. + appRunner: () => runApp(mainApp())); + } else { + runApp(mainApp()); + } +} + +MultiBlocProvider mainApp() { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => BotsBloc()..add(GetAllBots()), + ), + BlocProvider( + create: (context) => ThemeModeCubit(), + ), + BlocProvider( + create: (context) => CategoryCubit()..getAllCategorie(), + ), + BlocProvider( + create: (context) => UserInfoCubit(), + ), + BlocProvider( + create: (context) => ChatsHistoryBloc(), + ), + BlocProvider( + create: (context) => PaymantHistoryBloc()..add(GetAllHistory()), + ), + BlocProvider( + create: (context) => ToolsBloc()..add(GetAllTools()), + ), + BlocProvider( + create: (context) => PersonalAssistantsBloc()..add(GetAll()), + ), + BlocProvider( + create: (context) => + GlobalAssistantsBloc()..add(const GetGlobalAssistants()), + ), + BlocProvider( + create: (context) => PersonsCubit()..getCharacters(), + ), + BlocProvider( + create: (context) => PostsCubit()..getLastPosts(), + ), + BlocProvider( + create: (context) => CmpCubit()..getAllEvents(), + ), + BlocProvider( + create: (context) => MediasCubit()..getMedias(), + ), + BlocProvider( + create: (context) => SingleMediaCubit(), + ), + BlocProvider( + create: (context) => MainChatBotCubit()..getBot(1), + ), + BlocProvider( + create: (context) => OnShowAddCubit(), + ), + BlocProvider( + create: (context) => AdRemainingCubit()..getRemainingAd(), + ) + ], + child: const MyApp(), + ); +} + +final GlobalKey navigatorKey = GlobalKey(); + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final isDark = state == ThemeMode.dark; + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( + systemNavigationBarContrastEnforced: false, + + systemNavigationBarColor: isDark + ? darkThemeDefault.scaffoldBackgroundColor + : lightDefaultTheme + .scaffoldBackgroundColor, // Change the bottom navigation bar color + systemNavigationBarIconBrightness: isDark + ? Brightness.light + : Brightness.dark, // Change icon color (light/dark) + )); + return Container( + color: isDark + ? darkThemeDefault.scaffoldBackgroundColor + : lightDefaultTheme.scaffoldBackgroundColor, + child: Padding( + padding: + EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), + child: Material( + child: MaterialApp.router( + title: 'هوشان', + debugShowCheckedModeBanner: false, + // initialRoute: Routes.main, + // navigatorKey: navigatorKey, + // onGenerateRoute: Routes.router, + routerConfig: Routes.routeGenerator, + scrollBehavior: MyCustomScrollBehavior(), + themeMode: state, + theme: isDark ? darkThemeDefault : lightDefaultTheme, + ), + ).animate().fadeIn(duration: 400.ms), + ), + ); + }, + ); + } +} diff --git a/lib/ui/screens/assistant/assistant_page.dart b/lib/ui/screens/assistant/assistant_page.dart new file mode 100644 index 0000000..f51ac34 --- /dev/null +++ b/lib/ui/screens/assistant/assistant_page.dart @@ -0,0 +1,1302 @@ +// ignore_for_file: deprecated_member_use_from_same_package, use_build_context_synchronously + +import 'dart:math'; + +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/core/utils/date_time.dart'; +import 'package:hoshan/core/utils/strings.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/data/model/assistant_comments_model.dart'; +import 'package:hoshan/data/model/chat_args.dart'; +import 'package:hoshan/data/model/edittext_state_model.dart'; +import 'package:hoshan/data/repository/bot_repository.dart'; +import 'package:hoshan/ui/screens/assistant/bloc/assistant_info_bloc.dart'; +import 'package:hoshan/ui/screens/assistant/bloc/same_assistants_bloc.dart'; +import 'package:hoshan/ui/screens/assistant/cubit/assistant_comments_cubit.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/bot/bot_grid_card.dart'; +import 'package:hoshan/ui/widgets/components/bot/bot_grid_card_placeholder.dart'; +import 'package:hoshan/ui/widgets/components/button/circle_icon_btn.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/dialog/bottom_sheets.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; +import 'package:hoshan/ui/widgets/components/text/credit_cost.dart'; +import 'package:hoshan/ui/widgets/components/text/labeled_text_field.dart'; +import 'package:hoshan/ui/widgets/sections/header/reversible_appbar.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; +import 'package:persian_number_utility/persian_number_utility.dart'; + +class AssistantPage extends StatefulWidget { + const AssistantPage({super.key}); + + @override + State createState() => _AssistantPageState(); +} + +class _AssistantPageState extends State { + EdittextStateModel commentState = EdittextStateModel( + label: 'گوشمون به شماست!', + hintText: 'از نظر شما این دستیار چقدر عالی کار کرده؟', + ); + double score = 0; + ValueNotifier loadingAddComment = ValueNotifier(false); + ValueNotifier errorAddComment = ValueNotifier(null); + ValueNotifier maxLines = ValueNotifier(5); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: ReversibleAppbar( + context, + title: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: CreditCost( + call: false, + loadingColor: Theme.of(context).colorScheme.secondary, + ), + ), + const SizedBox( + width: 8, + ), + CircleIconBtn( + icon: Assets.icon.outline.coin, + color: AppColors.secondryColor[50], + iconColor: AppColors.secondryColor.defaultShade, + ), + ], + ), + ), + body: BlocConsumer( + listener: (context, state) { + if (state is AssistantInfoSuccess) { + context.read().add(GetSameAssistants( + id: state.assistantInfo.category!.id!, + botId: state.assistantInfo.id)); + context.read().loadComments( + id: state.assistantInfo.id!, + ); + } else if (state is AssistantInfoFail) { + context.pop(); + SnackBarManager(context, id: 'GetAssistantError').show( + status: SnackBarStatus.error, + message: 'خطا در بارگذاری دستیار'); + } + }, + builder: (context, state) { + if (state is AssistantInfoSuccess) { + final info = state.assistantInfo; + + ValueNotifier comments = ValueNotifier(info.comments ?? 0); + ValueNotifier scoreOfInfo = ValueNotifier(info.score ?? 0); + UserComment? userComment = info.userComment; + // commentState.formController.text = info.userComment?.text ?? ''; + // if (info.userComment != null) { + // score = info.userComment!.score ?? score; + // } + return Responsive(context).maxWidthInDesktop( + child: (contxet, maxWidth) => SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Directionality( + textDirection: TextDirection.rtl, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + flex: 3, + child: Row( + children: [ + ImageNetwork( + width: 62, + height: 62, + radius: 360, + baseUrl: DioService.baseURL, + url: info.image), + const SizedBox( + width: 8, + ), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + info.name ?? '', + style: AppTextStyles.headline6 + .copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Row( + children: [ + Assets + .icon.outline.elementPlus + .svg( + color: + Theme.of(context) + .colorScheme + .onSurface), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + 'دسته‌بندی: ${info.category?.name ?? ''}', + style: AppTextStyles.body4 + .copyWith( + color: Theme.of( + context) + .colorScheme + .onSurface), + maxLines: 1, + overflow: + TextOverflow.ellipsis, + ), + ) + ], + ) + ], + ), + ) + ], + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: + AppColors.green.defaultShade, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + Padding( + padding: + const EdgeInsets.only( + top: 4.0), + child: ValueListenableBuilder( + valueListenable: + scoreOfInfo, + builder: + (context, sc, _) { + return Text( + sc + .toStringAsFixed( + 1) + .padRight(1, '0'), + style: AppTextStyles + .body4 + .copyWith( + fontWeight: + FontWeight + .bold, + color: Colors + .white), + ); + }), + ), + const Icon( + Icons.star_rate_rounded, + color: Colors.white, + ), + ], + ), + ), + Container( + width: double.infinity, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .surface, + ), + child: Center( + child: ValueListenableBuilder( + valueListenable: comments, + builder: (context, cms, _) { + return Text( + '$cms نظر', + style: AppTextStyles.body4 + .copyWith( + fontWeight: + FontWeight + .bold, + color: Theme.of( + context) + .colorScheme + .onSurface), + ); + }), + ), + ), + ], + ), + ), + ), + ], + ), + const SizedBox( + height: 12, + ), + Row( + children: [ + Expanded( + child: infoContainer( + title: 'کاربران این دستیار', + description: + '${info.users ?? 10} کاربر'), + ), + Expanded( + child: infoContainer( + title: 'تعداد پیام‌ها', + description: + '${info.messages ?? 20} پیام'), + ), + ], + ), + Row( + children: [ + Expanded( + child: infoContainer( + title: 'سازنده دستیار', + description: + info.user?.username ?? 'هوشان'), + ), + Expanded( + child: infoContainer( + title: 'تاریخ ایجاد', + description: DateTimeUtils + .convertStringIsoToDate( + info.createdAt!) + .toPersianDate()), + ), + ], + ), + if (info.description != null) + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: LayoutBuilder( + builder: (context, constraints) { + return ValueListenableBuilder( + valueListenable: maxLines, + builder: (context, lines, _) { + final span = TextSpan( + text: info.description, + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[context + .read< + ThemeModeCubit>() + .isDark() + ? 600 + : 900])); + final tp = TextPainter( + text: span, + textDirection: TextDirection.ltr); + tp.layout( + maxWidth: constraints.maxWidth); + final numLines = + tp.computeLineMetrics().length; + return Padding( + padding: const EdgeInsets.fromLTRB( + 4, 12, 4, 4), + child: Stack( + children: [ + Column( + children: [ + Text( + info.description!, + style: AppTextStyles.body4 + .copyWith( + color: AppColors + .gray[context + .read< + ThemeModeCubit>() + .isDark() + ? 600 + : 900]), + maxLines: (lines), + textAlign: + TextAlign.justify, + ), + if (lines == null && + numLines >= 5) + Transform.rotate( + angle: -pi / 2, + child: CircleIconBtn( + onTap: () { + if (maxLines + .value == + null) { + maxLines + .value = 5; + return; + } + maxLines.value = + null; + }, + size: 46, + color: Theme.of( + context) + .colorScheme + .primary, + iconColor: + Colors.white, + icon: Assets + .icon + .outline + .arrowRight)), + ], + ), + if (lines != null && + numLines > lines) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + height: 64, + decoration: + BoxDecoration( + gradient: + LinearGradient( + colors: [ + Theme.of(context) + .scaffoldBackgroundColor, + Theme.of(context) + .scaffoldBackgroundColor + .withAlpha( + 140) + ], + begin: Alignment + .bottomCenter, + end: Alignment + .topCenter, + ), + ), + alignment: Alignment + .bottomCenter, + child: SizedBox( + child: Transform.rotate( + angle: pi / 2, + child: CircleIconBtn( + onTap: () { + if (maxLines + .value == + null) { + maxLines + .value = 5; + return; + } + maxLines.value = + null; + }, + size: 46, + color: Theme.of(context).colorScheme.primary, + iconColor: Colors.white, + icon: Assets.icon.outline.arrowRight)), + ), + )) + ], + ), + ); + }); + }), + ), + const SizedBox( + height: 12, + ), + LoadingButton( + onPressed: () { + contxet.go(Routes.chatFromAssistant, + extra: + ChatArgs(bot: state.assistantInfo)); + }, + color: AppColors.primaryColor.defaultShade, + radius: 16, + width: double.infinity, + height: 48, + child: Text( + 'استفاده از دستیار', + style: AppTextStyles.body3 + .copyWith(color: Colors.white), + )), + const SizedBox( + height: 12, + ), + LoadingButton( + color: Theme.of(context).colorScheme.secondary, + isOutlined: true, + height: 48, + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + width: double.infinity, + onPressed: () { + BottomSheetHandler(context) + .showReportOptions(); + }, + child: Text( + 'گزارش اشکال', + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context) + .colorScheme + .secondary), + ), + ), + const SizedBox( + height: 12, + ), + ValueListenableBuilder( + valueListenable: loadingAddComment, + builder: (context, loading, _) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .surface, + borderRadius: + BorderRadius.circular(8)), + padding: const EdgeInsets.all(16), + child: Stack( + children: [ + IgnorePointer( + ignoring: loading, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + Center( + child: Directionality( + textDirection: + TextDirection.ltr, + child: RatingBar( + initialRating: 1, + direction: + Axis.horizontal, + allowHalfRating: true, + itemCount: 5, + itemSize: 46, + minRating: 1, + maxRating: 5, + glow: false, + ratingWidget: + RatingWidget( + full: Icon( + Icons + .star_rate_rounded, + color: AppColors.green + .defaultShade, + ), + half: Icon( + Icons + .star_half_rounded, + color: AppColors.green + .defaultShade, + ), + empty: Icon( + Icons + .star_outline_rounded, + color: AppColors.gray + .defaultShade, + ), + ), + itemPadding: + EdgeInsets.zero, + onRatingUpdate: (rating) { + score = rating; + }, + ), + ), + ), + const SizedBox( + height: 16, + ), + LabeledTextField( + stateController: commentState, + onValid: (value) { + if (value != null && + value.isEmpty) { + return 'لطفا فیلد نظر را پر کنید!'; + } + return null; + }, + ), + const SizedBox( + height: 12, + ), + TextButton( + onPressed: () async { + if (!commentState + .formState + .currentState! + .validate()) { + return; + } else if (score < 1) { + SnackBarManager(context, + id: + 'score-fail') + .show( + message: + 'حداقل امتیاز وازد شده باید 1 باشد', + status: + SnackBarStatus + .error); + } + loadingAddComment.value = + true; + errorAddComment.value = + null; + try { + AssistantComments comment = AssistantComments( + createdAt: DateTimeUtils + .getNow() + .toIso8601String(), + score: score, + text: commentState + .formController + .text, + user: AssistantCommentsUser( + image: UserInfoCubit + .userInfoModel + .image, + username: UserInfoCubit + .userInfoModel + .username)); + await BotRepository + .addCommentInAssistant( + state + .assistantInfo + .id!, + comment); + loadingAddComment + .value = false; + context + .read< + AssistantCommentsCubit>() + .removeComment( + username: UserInfoCubit + .userInfoModel + .username!); + context + .read< + AssistantCommentsCubit>() + .addComment( + comment: + comment); + SnackBarManager(context, + id: + 'addComment') + .show( + status: + SnackBarStatus + .success, + message: + 'نظر شما با موفقیت ارسال شد'); + + if (state.assistantInfo + .userComment != + null) { + scoreOfInfo + .value = (scoreOfInfo + .value * + comments + .value + + (score - + (userComment + ?.score ?? + 0))) / + comments.value; + } else { + scoreOfInfo + .value = ((scoreOfInfo + .value * + comments + .value) + + score) / + (comments.value + + 1); + comments.value = + comments.value + + 1; + } + userComment = UserComment( + score: score, + text: commentState + .formController + .text); + commentState + .formController + .clear(); + } on DioException catch (e) { + try { + errorAddComment + .value = + e.response?.data[ + 'detail']; + } catch (e) { + errorAddComment + .value = + 'مشکلی پیش آمده لحظاتی دیگر دوباره تلاش کنید'; + } + + if (kDebugMode) { + print( + 'Dio Error is: $e'); + } + } + loadingAddComment.value = + false; + }, + child: Padding( + padding: + const EdgeInsets.all( + 8.0), + child: Text( + state.assistantInfo + .userComment != + null + ? 'تغییر نظر' + : 'ثبت نظر', + style: AppTextStyles + .body3 + .copyWith( + color: Theme.of( + context) + .colorScheme + .primary), + ), + )) + ], + ), + ), + if (loading) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(16), + color: Theme.of(context) + .colorScheme + .surface + .withValues(alpha: 0.5)), + child: Center( + child: SpinKitThreeBounce( + color: AppColors.primaryColor + .defaultShade, + size: 32, + ), + ), + )) + ], + ), + ); + }), + const SizedBox( + height: 12, + ), + BlocBuilder( + builder: (context, commentState) { + if (commentState + is AssistantCommentsInitial) { + return Column( + children: [ + ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + 'نظرات کاربران', + style: AppTextStyles.body3.copyWith( + fontWeight: FontWeight.bold), + ), + ), + const SizedBox( + height: 12, + ), + ListView.builder( + physics: + const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: 3, + itemBuilder: (context, index) { + return commentContainerPlaceholder(); + }, + ), + ], + ); + } + if (commentState.comments.isNotEmpty) { + return Column( + children: [ + ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + 'نظرات کاربران', + style: AppTextStyles.body3.copyWith( + fontWeight: FontWeight.bold), + ), + ), + const SizedBox( + height: 12, + ), + ListView.builder( + physics: + const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: + commentState.comments.length, + itemBuilder: (context, index) { + final comment = + commentState.comments[index]; + return commentContainer(comment); + }, + ), + const SizedBox( + height: 12, + ), + if (commentState + is AssistantCommentsLoading) + Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0), + child: LinearProgressIndicator( + color: AppColors + .primaryColor.defaultShade, + borderRadius: + BorderRadius.circular(8), + ), + ), + if ((commentState + is AssistantCommentsSuccess || + commentState + is AssistantCommentsFail) && + !(commentState.lastPage != null && + commentState.page > + commentState.lastPage!)) + GestureDetector( + onTap: () { + if (context + .read< + AssistantCommentsCubit>() + .state + is AssistantCommentsLoading || + context + .read< + AssistantCommentsCubit>() + .state + is AssistantCommentsInitial) { + return; + } + context + .read< + AssistantCommentsCubit>() + .loadComments( + id: state + .assistantInfo.id!); + }, + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + 'مشاهده بیشتر', + style: AppTextStyles.body3 + .copyWith( + fontWeight: + FontWeight.bold), + ), + const SizedBox( + width: 8, + ), + Transform.rotate( + angle: pi / 2, + child: Assets + .icon.outline.arrowRight + .svg( + color: AppColors + .secondryColor + .defaultShade)) + ], + ), + ), + ], + ); + } + return const SizedBox.shrink(); + }, + ), + const SizedBox( + height: 24, + ), + ValueListenableBuilder( + valueListenable: errorAddComment, + builder: (context, message, child) { + if (message != null && message.isNotEmpty) { + return Container( + decoration: BoxDecoration( + color: AppColors.red[50], + borderRadius: + BorderRadius.circular(16)), + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + Icon( + Icons.warning_rounded, + color: AppColors.red.defaultShade, + size: 32, + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + message, + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[context + .read< + ThemeModeCubit>() + .isDark() + ? 600 + : 900]), + ), + ), + ], + ), + ); + } + return const SizedBox.shrink(); + }, + ) + ], + ), + ), + const SizedBox( + height: 12, + ), + BlocBuilder( + builder: (context, state) { + if (state is SameAssistantsFail) { + return const SizedBox.shrink(); + } + if (state is SameAssistantsSuccess) { + if (state.bots.isEmpty) { + return const SizedBox.shrink(); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + child: ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + 'دستیارهای مشابه', + style: AppTextStyles.body3.copyWith( + fontWeight: FontWeight.bold), + ), + ), + ), + SizedBox( + height: 170, + child: ListView.builder( + shrinkWrap: true, + itemCount: state.bots.length, + padding: const EdgeInsets.symmetric( + horizontal: 8), + physics: const BouncingScrollPhysics(), + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + return SizedBox( + width: (Responsive(context) + .isDesktop() + ? 300 + : MediaQuery.sizeOf(context) + .width) * + 0.5, + child: Padding( + padding: + const EdgeInsets.symmetric( + horizontal: 4.0, + vertical: 8), + child: GestureDetector( + onTap: () { + contxet.push(Routes.assistant, + extra: + state.bots[index].id); + }, + child: BotGridCard( + bot: state.bots[index], + ), + ), + )); + }, + ), + ), + ], + ); + } + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + child: DefaultPlaceHolder( + child: ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + 'دستیارهای مشابه', + style: AppTextStyles.body3.copyWith( + fontWeight: FontWeight.bold), + ), + ), + ), + ), + SizedBox( + height: 170, + child: ListView.builder( + shrinkWrap: true, + itemCount: 10, + padding: const EdgeInsets.symmetric( + horizontal: 16), + physics: + const NeverScrollableScrollPhysics(), + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + return SizedBox( + width: (Responsive(context) + .isDesktop() + ? 300 + : MediaQuery.sizeOf(context) + .width) * + 0.5, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4.0, vertical: 8), + child: GestureDetector( + onTap: () {}, + child: BotGridCardPlaceholder( + index: index, + ), + ), + )); + }, + ), + ), + ], + ); + }, + ), + const SizedBox( + height: 12, + ), + ], + ), + ), + ), + ), + ); + } + return Center( + child: SpinKitThreeBounce( + size: 32, + color: AppColors.primaryColor.defaultShade, + ), + ); + }, + ), + ); + } + + Container commentContainerPlaceholder() { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + border: Border.all(color: AppColors.gray[600]), + color: Theme.of(context).colorScheme.surface), + padding: const EdgeInsetsDirectional.all(16), + margin: const EdgeInsets.symmetric(vertical: 4), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + DefaultPlaceHolder( + child: Container( + width: 52, + height: 52, + margin: const EdgeInsets.all(8), + decoration: const BoxDecoration( + shape: BoxShape.circle, color: Colors.white), + ), + ), + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Colors.white), + child: Text( + 'کدباز 2025', + style: AppTextStyles.body4 + .copyWith(fontWeight: FontWeight.bold), + ), + ), + ) + ], + ), + DefaultPlaceHolder( + child: Directionality( + textDirection: TextDirection.ltr, + child: RatingBar( + initialRating: 3, + direction: Axis.horizontal, + allowHalfRating: true, + itemCount: 5, + itemSize: 24, + ignoreGestures: true, + ratingWidget: RatingWidget( + full: Icon( + Icons.star_rate_rounded, + color: AppColors.green.defaultShade, + ), + half: Icon( + Icons.star_half_rounded, + color: AppColors.green.defaultShade, + ), + empty: Icon( + Icons.star_outline_rounded, + color: AppColors.green.defaultShade, + ), + ), + itemPadding: EdgeInsets.zero, + onRatingUpdate: (rating) {}, + ), + ), + ) + ], + ), + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), color: Colors.white), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + '"این دستیار برنامه‌نویسی خیلی به کارم اومد! باگ‌هایی رو که کلی وقت می‌گرفتن، سریع پیدا می‌کنه و حتی راه‌حل‌های پیشنهادی می‌ده".👌🤖', + style: + AppTextStyles.body4.copyWith(fontWeight: FontWeight.w500), + ), + ), + ), + ) + ], + ), + ); + } + + Container commentContainer(final AssistantComments comment) { + final daysAgo = DateTimeUtils.getDaysBetweenNowAnd(comment.createdAt!); + + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + border: Border.all(color: AppColors.gray[600]), + color: Theme.of(context).colorScheme.surface), + padding: const EdgeInsetsDirectional.all(8), + margin: const EdgeInsets.symmetric(vertical: 4), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + ImageNetwork( + url: comment.user?.image ?? '', + width: 52, + height: 52, + radius: 360, + ), + const SizedBox( + width: 8, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + comment.user?.username ?? '', + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface), + ), + Text( + daysAgo == 0 ? 'امروز' : '$daysAgo روز قبل', + style: AppTextStyles.body6 + .copyWith(color: AppColors.gray[700]), + ) + ], + ) + ], + ), + Directionality( + textDirection: TextDirection.ltr, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Opacity( + opacity: (comment.score ?? 0) == 0 ? 0 : 1, + child: RatingBar( + initialRating: comment.score ?? 0, + direction: Axis.horizontal, + allowHalfRating: true, + itemCount: 5, + itemSize: 24, + ignoreGestures: true, + ratingWidget: RatingWidget( + full: Icon( + Icons.star_rate_rounded, + color: AppColors.green.defaultShade, + ), + half: Icon( + Icons.star_half_rounded, + color: AppColors.green.defaultShade, + ), + empty: Icon( + Icons.star_outline_rounded, + color: AppColors.green.defaultShade, + ), + ), + itemPadding: EdgeInsets.zero, + onRatingUpdate: (rating) {}, + ), + ), + const SizedBox( + height: 4, + ), + InkWell( + onTap: BottomSheetHandler(context).showReportOptions, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Assets.icon.outline.flag2 + .svg(color: AppColors.gray[700]), + const SizedBox( + width: 4, + ), + Text( + 'گزارش', + style: AppTextStyles.body4 + .copyWith(color: AppColors.gray[700]), + ), + ], + ), + ) + ], + ), + ) + ], + ), + const SizedBox( + height: 8, + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + width: double.infinity, + child: Text( + comment.text ?? '', + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface), + textDirection: + comment.text != null && comment.text!.startsWithEnglish() + ? TextDirection.ltr + : TextDirection.rtl, + ), + ), + ) + ], + ), + ); + } + + Container infoContainer( + {required final String title, required final String description}) { + return Container( + margin: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.all(8), + child: Column( + children: [ + Text( + title, + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox( + height: 8, + ), + Text( + description, + style: AppTextStyles.body5 + .copyWith(color: Theme.of(context).colorScheme.primary), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + ], + ), + ); + } +} diff --git a/lib/ui/screens/assistant/bloc/assistant_info_bloc.dart b/lib/ui/screens/assistant/bloc/assistant_info_bloc.dart new file mode 100644 index 0000000..3ddc093 --- /dev/null +++ b/lib/ui/screens/assistant/bloc/assistant_info_bloc.dart @@ -0,0 +1,34 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/data/repository/bot_repository.dart'; + +part 'assistant_info_event.dart'; +part 'assistant_info_state.dart'; + +class AssistantInfoBloc extends Bloc { + AssistantInfoBloc() : super(AssistantInfoInitial()) { + on((event, emit) async { + if (event is Getinfo) { + emit(AssistantInfoLoading()); + try { + final response = await BotRepository.getAssistantGlobalInfo(event.id); + emit(AssistantInfoSuccess(assistantInfo: response)); + } on DioException catch (e) { + emit(AssistantInfoFail()); + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } + if (event is ChangeInfo) { + emit(AssistantInfoLoading()); + + final info = event.info; + emit(AssistantInfoSuccess(assistantInfo: info)); + } + }); + } +} diff --git a/lib/ui/screens/assistant/bloc/assistant_info_event.dart b/lib/ui/screens/assistant/bloc/assistant_info_event.dart new file mode 100644 index 0000000..2b40d9f --- /dev/null +++ b/lib/ui/screens/assistant/bloc/assistant_info_event.dart @@ -0,0 +1,20 @@ +part of 'assistant_info_bloc.dart'; + +sealed class AssistantInfoEvent extends Equatable { + const AssistantInfoEvent(); + + @override + List get props => []; +} + +class Getinfo extends AssistantInfoEvent { + final int id; + + const Getinfo({required this.id}); +} + +class ChangeInfo extends AssistantInfoEvent { + final Bots info; + + const ChangeInfo({required this.info}); +} diff --git a/lib/ui/screens/assistant/bloc/assistant_info_state.dart b/lib/ui/screens/assistant/bloc/assistant_info_state.dart new file mode 100644 index 0000000..7c91a05 --- /dev/null +++ b/lib/ui/screens/assistant/bloc/assistant_info_state.dart @@ -0,0 +1,20 @@ +part of 'assistant_info_bloc.dart'; + +sealed class AssistantInfoState extends Equatable { + const AssistantInfoState(); + + @override + List get props => []; +} + +final class AssistantInfoInitial extends AssistantInfoState {} + +final class AssistantInfoLoading extends AssistantInfoState {} + +final class AssistantInfoFail extends AssistantInfoState {} + +final class AssistantInfoSuccess extends AssistantInfoState { + final Bots assistantInfo; + + const AssistantInfoSuccess({required this.assistantInfo}); +} diff --git a/lib/ui/screens/assistant/bloc/same_assistants_bloc.dart b/lib/ui/screens/assistant/bloc/same_assistants_bloc.dart new file mode 100644 index 0000000..fefa45a --- /dev/null +++ b/lib/ui/screens/assistant/bloc/same_assistants_bloc.dart @@ -0,0 +1,42 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/data/repository/bot_repository.dart'; + +part 'same_assistants_event.dart'; +part 'same_assistants_state.dart'; + +class SameAssistantsBloc + extends Bloc { + SameAssistantsBloc() : super(SameAssistantsInitial()) { + on((event, emit) async { + if (event is GetSameAssistants) { + emit(SameAssistantsLoading()); + try { + final cats = await BotRepository.getGlobalAssistant( + marked: false, categorieId: event.id); + if (event.botId != null) { + try { + cats.categories!.first.bots!.removeWhere( + (element) => element.id == event.botId, + ); + emit(SameAssistantsSuccess(bots: cats.categories!.first.bots!)); + } catch (e) { + emit(SameAssistantsFail()); + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } + } on DioException catch (e) { + emit(SameAssistantsFail()); + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } + }); + } +} diff --git a/lib/ui/screens/assistant/bloc/same_assistants_event.dart b/lib/ui/screens/assistant/bloc/same_assistants_event.dart new file mode 100644 index 0000000..0ab1e6a --- /dev/null +++ b/lib/ui/screens/assistant/bloc/same_assistants_event.dart @@ -0,0 +1,15 @@ +part of 'same_assistants_bloc.dart'; + +sealed class SameAssistantsEvent extends Equatable { + const SameAssistantsEvent(); + + @override + List get props => []; +} + +class GetSameAssistants extends SameAssistantsEvent { + final int id; + final int? botId; + + const GetSameAssistants({required this.id, this.botId}); +} diff --git a/lib/ui/screens/assistant/bloc/same_assistants_state.dart b/lib/ui/screens/assistant/bloc/same_assistants_state.dart new file mode 100644 index 0000000..31db936 --- /dev/null +++ b/lib/ui/screens/assistant/bloc/same_assistants_state.dart @@ -0,0 +1,20 @@ +part of 'same_assistants_bloc.dart'; + +sealed class SameAssistantsState extends Equatable { + const SameAssistantsState(); + + @override + List get props => []; +} + +final class SameAssistantsInitial extends SameAssistantsState {} + +final class SameAssistantsLoading extends SameAssistantsState {} + +final class SameAssistantsSuccess extends SameAssistantsState { + final List bots; + + const SameAssistantsSuccess({required this.bots}); +} + +final class SameAssistantsFail extends SameAssistantsState {} diff --git a/lib/ui/screens/assistant/cubit/assistant_comments_cubit.dart b/lib/ui/screens/assistant/cubit/assistant_comments_cubit.dart new file mode 100644 index 0000000..adb2d30 --- /dev/null +++ b/lib/ui/screens/assistant/cubit/assistant_comments_cubit.dart @@ -0,0 +1,88 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/assistant_comments_model.dart'; +import 'package:hoshan/data/repository/bot_repository.dart'; + +part 'assistant_comments_state.dart'; + +class AssistantCommentsCubit extends Cubit { + AssistantCommentsCubit() : super(AssistantCommentsInitial()); + static final List comments = []; + + void loadComments({required final int id}) async { + int page = state.page; + if (page == 1) { + comments.clear(); + } + int? lastPage = state.lastPage; + if (lastPage != null && page > lastPage) { + return; + } else if (page != 1) { + emit(AssistantCommentsLoading( + comments: comments, lastPage: lastPage, page: page)); + } + + try { + final cModel = await BotRepository.getAssistantComments(id, page); + + page++; + lastPage = cModel.lastPage; + comments.addAll(cModel.comments!); + emit(AssistantCommentsSuccess( + comments: comments, lastPage: lastPage, page: page)); + } on DioException catch (e) { + emit(AssistantCommentsFail( + comments: comments, lastPage: lastPage, page: page)); + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } + + void addComment({required final AssistantComments comment}) { + emit(AssistantCommentsLoading( + comments: comments, lastPage: state.lastPage, page: state.page)); + comments.insert(0, comment); + emit(AssistantCommentsSuccess( + comments: comments, lastPage: state.lastPage, page: state.page)); + } + + void removeComment({required final String username}) { + emit(AssistantCommentsLoading( + comments: comments, lastPage: state.lastPage, page: state.page)); + + comments.removeWhere( + (element) => element.user!.username == username, + ); + emit(AssistantCommentsSuccess( + comments: comments, lastPage: state.lastPage, page: state.page)); + } + + // void changeComment({ + // required final Comment newComment, + // }) { + // emit(CommentsLoading(comments: comments)); + // final index = comments.indexWhere( + // (element) => element.id == newComment.id, + // ); + // comments[index] = newComment; + // emit(CommentsSuccess(comments: comments)); + // } + + // void addReplies({required final int commentId}) { + // emit(CommentsLoading(comments: comments)); + // final comment = comments.firstWhere( + // (element) => element.id == commentId, + // ); + // final index = comments.indexOf(comment); + // if (comment.replies == null) { + // comment.replies = 1; + // } else { + // comment.replies = comment.replies! + 1; + // } + // comments[index] = comment; + // emit(CommentsSuccess(comments: comments)); + // } +} diff --git a/lib/ui/screens/assistant/cubit/assistant_comments_state.dart b/lib/ui/screens/assistant/cubit/assistant_comments_state.dart new file mode 100644 index 0000000..75f54ca --- /dev/null +++ b/lib/ui/screens/assistant/cubit/assistant_comments_state.dart @@ -0,0 +1,27 @@ +part of 'assistant_comments_cubit.dart'; + +sealed class AssistantCommentsState extends Equatable { + final List comments; + final int page; + final int? lastPage; + + const AssistantCommentsState( + {this.comments = const [], this.page = 1, this.lastPage}); + + @override + List get props => [comments, page, lastPage ?? 0]; +} + +final class AssistantCommentsInitial extends AssistantCommentsState {} + +final class AssistantCommentsLoading extends AssistantCommentsState { + const AssistantCommentsLoading({super.comments, super.page, super.lastPage}); +} + +final class AssistantCommentsFail extends AssistantCommentsState { + const AssistantCommentsFail({super.comments, super.page, super.lastPage}); +} + +final class AssistantCommentsSuccess extends AssistantCommentsState { + const AssistantCommentsSuccess({super.comments, super.page, super.lastPage}); +} diff --git a/lib/ui/screens/auth/auth_page.dart b/lib/ui/screens/auth/auth_page.dart new file mode 100644 index 0000000..fea54fc --- /dev/null +++ b/lib/ui/screens/auth/auth_page.dart @@ -0,0 +1,333 @@ +// ignore_for_file: deprecated_member_use_from_same_package, deprecated_member_use + +import 'dart:async'; + +import 'package:app_links/app_links.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/data/model/auth/auth_screens_enum.dart'; +import 'package:hoshan/data/storage/shared_preferences_helper.dart'; +import 'package:hoshan/ui/screens/auth/code/bloc/check_code_bloc.dart'; +import 'package:hoshan/ui/screens/auth/code/gift_code_screen.dart'; +import 'package:hoshan/ui/screens/auth/cubit/auth_screens_cubit.dart'; + +import 'package:hoshan/ui/screens/auth/on_boarding/on_boarding_page.dart'; +import 'package:hoshan/ui/screens/auth/register/bloc/register_bloc.dart'; +import 'package:hoshan/ui/screens/auth/register/register_screen.dart'; +import 'package:hoshan/ui/screens/auth/verification/bloc/verification_bloc.dart'; +import 'package:hoshan/ui/screens/auth/verification/verification_screen.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/circle_icon_btn.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class AuthPage extends StatefulWidget { + final bool show; + const AuthPage({super.key, this.show = true}); + + @override + State createState() => _AuthPageState(); +} + +class _AuthPageState extends State { + late bool onBoarding = widget.show; + StreamSubscription? _linkSubscription; + + @override + void initState() { + super.initState(); + _initDeepLinkListener(); + } + + void _initDeepLinkListener() { + if (!kIsWeb) { + final appLinks = AppLinks(); + _linkSubscription = appLinks.uriLinkStream.listen((uri) async { + if (uri.scheme == 'houshan' && uri.host == 'auth') { + final token = uri.queryParameters['token']; + if (token != null && token.isNotEmpty) { + if (kDebugMode) { + print('Deep link received while app open: $token'); + } + await AuthTokenStorage.setToken(token); + await OnBoardingStorage.setAsSeen(); + if (mounted) { + context.read().getUserInfo(); + context.go(Routes.main); + } + } + } + }); + } + } + + @override + void dispose() { + _linkSubscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + return context.read().state == + AuthScreens.verification || + context.read().state == AuthScreens.code + ? false + : true; + }, + child: Scaffold( + resizeToAvoidBottomInset: true, + bottomNavigationBar: + Responsive(context).isMobile() && onBoarding ? null : footer(), + body: Stack( + children: [ + Responsive(context).builder( + mobile: onBoarding + ? OnBoardingPage( + onFinish: () { + // OnBoardingStorage.setBoradingStatus(false); + setState(() { + onBoarding = false; + }); + }, + ) + : authScreen(), + desktop: Row( + children: [ + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.1, + ), + const Expanded(child: OnBoardingPage()), + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.1, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: authScreen(), + )), + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.1, + ), + ], + ), + ), + Positioned( + top: 46, + right: 16, + child: BlocBuilder( + builder: (context, state) { + return CircleIconBtn( + onTap: context.read().switchTheme, + icon: (state == ThemeMode.dark + ? Assets.icon.outline.moon + : Assets.icon.outline.sun), + iconColor: Theme.of(context).colorScheme.onSurface, + color: Theme.of(context).colorScheme.surface, + size: 40, + iconPadding: const EdgeInsets.all(10), + ); + }, + )) + ], + ), + ), + ); + } + + Widget authScreen() { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: + BoxConstraints(minHeight: MediaQuery.sizeOf(context).height * 0.85), + child: Column( + children: [ + const SizedBox( + height: 120, + ), + header(), + BlocBuilder( + builder: (context, state) { + Widget mainScreen; + if (state == AuthScreens.mobile) { + mainScreen = const RegisterScreen(); + } else if (state == AuthScreens.code) { + mainScreen = const GiftCodeScreen(); + } else { + mainScreen = const VerificationScreen(); + } + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 24, + ), + MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => RegisterBloc(), + ), + BlocProvider( + create: (context) => VerificationBloc(), + ), + BlocProvider( + create: (context) => CheckCodeBloc(), + ), + ], + child: mainScreen, + ), + + // if (state != AuthScreens.code && + // state != AuthScreens.verification) + // Column( + // children: [ + // Padding( + // padding: const EdgeInsets.symmetric(vertical: 16.0), + // child: Text( + // 'یا', + // style: AppTextStyles.body3, + // ), + // ), + // SizedBox( + // width: MediaQuery.sizeOf(context).width, + // height: 48, + // child: Padding( + // padding: + // const EdgeInsets.symmetric(horizontal: 16.0), + // child: ElevatedButton( + // style: ElevatedButton.styleFrom( + // backgroundColor: Colors.white, + // shape: RoundedRectangleBorder( + // side: BorderSide( + // color: AppColors + // .primaryColor.defaultShade, + // width: 1.5), + // borderRadius: + // BorderRadius.circular(100)), + // ), + // onPressed: () async { + // try { + // await AuthService().signInWithGoogle(); + // } on PlatformException catch (e) { + // if (e.code == 'sign_in_failed') { + // throw Exception( + // 'Google Sign In failed. Please check your Google account and try again.'); + // } else { + // throw Exception( + // 'An error occurred while signing in with Google: ${e.message}'); + // } + // } catch (e) { + // throw Exception( + // 'An error occurred while signing in with Google: ${e.toString()}'); + // } + // }, + // child: Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Text( + // 'ورود با حساب گوگل', + // style: AppTextStyles.body4.copyWith( + // color: AppColors + // .primaryColor.defaultShade), + // ), + // const SizedBox( + // width: 8, + // ), + // Assets.icon.signin.igoogle.svg(), + // ], + // )), + // ), + // ), + // ], + // ) + ], + ); + }, + ) + ], + ), + ), + ); + } + + Center header() { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Assets.icon.launcherIcons.houshanIconPrimary + .image(width: 150, height: 150, fit: BoxFit.cover), + Text("هـوشان", + style: AppTextStyles.headline1.copyWith( + color: Theme.of(context).colorScheme.primary, + )), + SizedBox( + height: 15, + ), + Text( + '"ویژه کارکنان فولاد مبارکه و خانواده محترم آن‌ها"', + style: AppTextStyles.headline1.copyWith( + fontWeight: FontWeight.bold, + fontSize: 16, + color: context.read().isDark() + ? Colors.white70 + : Color.fromARGB(255, 61, 61, 61)), + ) + ], + ), + ), + ); + } + + Padding footer() { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: RichText( + textAlign: TextAlign.center, + textDirection: TextDirection.rtl, + text: TextSpan( + text: 'با ثبت نام و ورود به اپلیکیشن هوشان', + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[ + context.read().isDark() ? 600 : 900]), + children: [ + const TextSpan(text: '، \n'), + TextSpan( + text: 'شرایط و قوانین حریم خصوصی', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () async { + await launchUrl( + Uri.parse( + 'https://houshan.ai/%D9%82%D9%88%D8%A7%D9%86%DB%8C%D9%86-%D9%88-%D9%85%D9%82%D8%B1%D8%A7%D8%B1%D8%AA-%D8%AD%D9%81%D8%B8-%D8%AD%D8%B1%DB%8C%D9%85-%D8%AE%D8%B5%D9%88%D8%B5%DB%8C-%DA%A9%D8%A7%D8%B1%D8%A8%D8%B1%D8%A7%D9%86/'), + mode: LaunchMode.externalApplication) + .onError( + (error, stackTrace) { + if (kDebugMode) { + print('error open Link is: $error'); + } + return false; + }, + ); + }), + const TextSpan(text: ' را می‌‌پذیرم.'), + ])), + ); + } +} diff --git a/lib/ui/screens/auth/code/bloc/check_code_bloc.dart b/lib/ui/screens/auth/code/bloc/check_code_bloc.dart new file mode 100644 index 0000000..056fd6f --- /dev/null +++ b/lib/ui/screens/auth/code/bloc/check_code_bloc.dart @@ -0,0 +1,23 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:hoshan/data/repository/auth_repository.dart'; + +part 'check_code_event.dart'; +part 'check_code_state.dart'; + +class CheckCodeBloc extends Bloc { + CheckCodeBloc() : super(CheckCodeInitial()) { + on((event, emit) async { + if (event is VerifyGiftCode) { + emit(CheckCodeLoading()); + try { + final message = await AuthRepository.checkGiftCode(event.code); + emit(CheckCodeSucceess(message: message)); + } on DioException catch (e) { + emit(CheckCodeFail(error: e.response!.data['detail'])); + } + } + }); + } +} diff --git a/lib/ui/screens/auth/code/bloc/check_code_event.dart b/lib/ui/screens/auth/code/bloc/check_code_event.dart new file mode 100644 index 0000000..8d12e0c --- /dev/null +++ b/lib/ui/screens/auth/code/bloc/check_code_event.dart @@ -0,0 +1,14 @@ +part of 'check_code_bloc.dart'; + +sealed class CheckCodeEvent extends Equatable { + const CheckCodeEvent(); + + @override + List get props => []; +} + +class VerifyGiftCode extends CheckCodeEvent { + final String code; + + const VerifyGiftCode({required this.code}); +} diff --git a/lib/ui/screens/auth/code/bloc/check_code_state.dart b/lib/ui/screens/auth/code/bloc/check_code_state.dart new file mode 100644 index 0000000..540bb9f --- /dev/null +++ b/lib/ui/screens/auth/code/bloc/check_code_state.dart @@ -0,0 +1,24 @@ +part of 'check_code_bloc.dart'; + +sealed class CheckCodeState extends Equatable { + const CheckCodeState(); + + @override + List get props => []; +} + +final class CheckCodeInitial extends CheckCodeState {} + +final class CheckCodeLoading extends CheckCodeState {} + +final class CheckCodeSucceess extends CheckCodeState { + final String? message; + + const CheckCodeSucceess({required this.message}); +} + +final class CheckCodeFail extends CheckCodeState { + final String error; + + const CheckCodeFail({required this.error}); +} diff --git a/lib/ui/screens/auth/code/gift_code_screen.dart b/lib/ui/screens/auth/code/gift_code_screen.dart new file mode 100644 index 0000000..1a85bcf --- /dev/null +++ b/lib/ui/screens/auth/code/gift_code_screen.dart @@ -0,0 +1,125 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/data/model/home_args.dart'; +import 'package:hoshan/ui/screens/auth/code/bloc/check_code_bloc.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/dropdown/hint_tooltip.dart'; +import 'package:hoshan/ui/widgets/components/text/auth_text_field.dart'; + +class GiftCodeScreen extends StatefulWidget { + const GiftCodeScreen({super.key}); + + @override + State createState() => _GiftCodeScreenState(); +} + +class _GiftCodeScreenState extends State { + final TextEditingController textEditingController = TextEditingController(); + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) async { + if (state is CheckCodeSucceess) { + await context.read().getUserInfo(); + context.go(Routes.giftCredit, + extra: HomeArgs(message: state.message, freeCredit: 50)); + } + }, + builder: (context, state) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const HintTooltip( + hint: + 'کد معرف هوشان یک کد یکتا است که در صورت به اشتراک‌گذاری این اپلیکیشن توسط دوستانتان با شما، از طریق پیامک برایتان ارسال می‌شود.'), + const SizedBox( + width: 4, + ), + Text( + 'آیا کد معرف دارید؟ اگر کد معرف دارید، وارد کنید.', + textDirection: TextDirection.rtl, + style: AppTextStyles.body5 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ), + ], + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: AuthTextField( + controller: textEditingController, + label: 'کد معرف', + enabled: state is! CheckCodeLoading, + error: state is CheckCodeFail + ? Text( + state.error, + style: AppTextStyles.body5 + .copyWith(color: AppColors.red.defaultShade), + ) + : null, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + LoadingButton( + width: MediaQuery.sizeOf(context).width, + height: 48, + radius: 360, + loading: state is CheckCodeLoading, + color: Theme.of(context).colorScheme.primary, + onPressed: () { + if (textEditingController.text.isEmpty || + state is CheckCodeLoading) { + return; + } + context.read().add( + VerifyGiftCode(code: textEditingController.text)); + }, + child: Text( + 'تایید', + style: AppTextStyles.body4.copyWith(color: Colors.white), + ), + ), + const SizedBox( + height: 12, + ), + LoadingButton( + width: MediaQuery.sizeOf(context).width, + height: 48, + radius: 360, + isOutlined: true, + color: Theme.of(context).colorScheme.primary, + onPressed: () async { + if (state is CheckCodeLoading) return; + await context.read().getUserInfo(); + context.go(Routes.giftCredit, + extra: HomeArgs(freeCredit: 50)); + }, + child: Text( + 'ورود به اپلیکیشن', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.primary), + ), + ), + ], + ), + ) + ], + ); + }, + ); + } +} diff --git a/lib/ui/screens/auth/cubit/auth_screens_cubit.dart b/lib/ui/screens/auth/cubit/auth_screens_cubit.dart new file mode 100644 index 0000000..ad966f7 --- /dev/null +++ b/lib/ui/screens/auth/cubit/auth_screens_cubit.dart @@ -0,0 +1,15 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/data/model/auth/auth_screens_enum.dart'; + +class AuthScreensCubit extends Cubit { + AuthScreensCubit() : super(AuthScreens.mobile); + + String username = ''; + String password = ''; + String otp = ''; + bool isNew = false; + + void changeState(AuthScreens authScreens) { + emit(authScreens); + } +} diff --git a/lib/ui/screens/auth/gift/gift_credit_screen.dart b/lib/ui/screens/auth/gift/gift_credit_screen.dart new file mode 100644 index 0000000..6e5268c --- /dev/null +++ b/lib/ui/screens/auth/gift/gift_credit_screen.dart @@ -0,0 +1,322 @@ +// ignore_for_file: use_build_context_synchronously, deprecated_member_use + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/data/model/home_args.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; + +class GiftCreditScreen extends StatelessWidget { + const GiftCreditScreen({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final isDark = theme.brightness == Brightness.dark; + final surfaceColor = colorScheme.surface; + final backgroundColor = colorScheme.background; + final onSurface = colorScheme.onSurface; + final cardBorder = colorScheme.outlineVariant; + final warningBg = isDark + ? colorScheme.errorContainer.withOpacity(0.2) + : const Color.fromARGB(255, 248, 231, 241); + final warningBorder = isDark + ? colorScheme.errorContainer + : const Color.fromARGB(255, 172, 18, 105); + + return Directionality( + textDirection: TextDirection.rtl, + child: Scaffold( + backgroundColor: backgroundColor, + body: SafeArea( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.symmetric( + vertical: 24, horizontal: 16), + decoration: BoxDecoration( + color: surfaceColor, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + border: Border.all(color: cardBorder), + ), + child: Column( + children: [ + SvgPicture.asset('assets/icon/outline/gift 2.svg'), + const SizedBox(height: 20), + Text( + 'بسته اعتبار هدیه', + style: AppTextStyles.headline5.copyWith( + color: onSurface, + ), + ), + // const SizedBox(height: 8), + // Text( + // '1000 سکه هوشان', + // style: AppTextStyles.headline3.copyWith( + // color: Theme.of(context).colorScheme.onSurface, + // fontSize: 16, + // fontWeight: FontWeight.normal), + // ), + ], + ), + ), + const SizedBox(height: 24), + Column( + children: [ + Row( + children: [ + SvgPicture.asset( + 'assets/icon/outline/add family.svg'), + const SizedBox(width: 12), + Text( + 'دعوت اعضای خانواده', + style: AppTextStyles.headline6.copyWith( + color: onSurface, + ), + ), + ], + ), + SizedBox( + height: 15, + ), + RichText( + text: TextSpan( + style: AppTextStyles.body5.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontFamily: AppTextStyles.defaultFontFamily, + ), + children: [ + const TextSpan(text: 'شما می‌توانید '), + TextSpan( + text: 'تا 5 نفر', + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + const TextSpan( + text: + ' از اعضای خانواده خود را به استفاده از هوشان دعوت کنید.', + ), + ], + ), + ), + ], + ), + const SizedBox(height: 24), + _buildFeatureCard( + context, + icon: 'assets/icon/outline/Houshan features.svg', + title: 'دسترسی کامل به هوشان', + description: 'تمامی فیچرهای سرگرمی و آموزش', + borderColor: cardBorder, + titleColor: onSurface, + descColor: onSurface.withOpacity(0.8), + ), + const SizedBox(height: 12), + _buildFeatureCard( + context, + icon: 'assets/icon/outline/wallet.svg', + title: 'کیف پول مشترک', + description: 'استفاده از سکه‌های مشترک خانواده', + borderColor: cardBorder, + titleColor: onSurface, + descColor: onSurface.withOpacity(0.8), + ), + const SizedBox(height: 12), + _buildFeatureCard( + context, + icon: 'assets/icon/outline/Houshan features.svg', + title: 'ابزارهای خلاقانه', + description: + 'تولید عکس، ویدیو، صدا، چت با هوش مصنوعی و ...', + borderColor: cardBorder, + titleColor: onSurface, + descColor: onSurface.withOpacity(0.8), + ), + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: warningBg, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: warningBorder), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset('assets/icon/outline/warning.svg'), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'نکته مهم', + style: AppTextStyles.headline6.copyWith( + color: onSurface, + ), + ), + const SizedBox(height: 8), + Text( + 'سکه‌های شما بین تمام اعضای خانواده مشترک است. هر عضو می‌تواند از این سکه‌ها استفاده کند و مانده سکه برای همه یکسان خواهد بود', + style: AppTextStyles.body5.copyWith( + color: onSurface, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + // کانتینر سفید داخل کادر قرمز + // Container( + // padding: const EdgeInsets.all(12), + // decoration: BoxDecoration( + // color: Colors.white, + // borderRadius: BorderRadius.circular(12), + // border: Border.all(color: AppColors.red[100]!), + // ), + // child: Row( + // children: [ + // Expanded( + // child: Center( + // child: Text.rich( + // TextSpan( + // // استایل پایه برای کل متن (رنگ و فونت) + // style: AppTextStyles.body5.copyWith( + // color: Color.fromARGB(255, 61, 61, 61), + // ), + // children: [ + // TextSpan( + // text: 'تمدید خودکار: ', + // // این قسمت پررنگ‌تر می‌شود + // style: const TextStyle( + // fontWeight: FontWeight + // .w900, // یا FontWeight.bold + // ), + // ), + // const TextSpan( + // text: + // '', + // // این قسمت معمولی باقی می‌ماند + // style: TextStyle( + // fontWeight: FontWeight.normal, + // ), + // ), + // ], + // ), + // textAlign: + // TextAlign.center, // وسط‌چین کردن متن + // ), + // ), + // ), + // ], + // ), + // ), + ], + ), + ), + const SizedBox(height: 32), + LoadingButton( + width: MediaQuery.sizeOf(context).width, + height: 60, + radius: 100, + color: Theme.of(context).colorScheme.primary, + onPressed: () async { + await context.read().getUserInfo(); + context.go(Routes.home, + extra: HomeArgs(freeCredit: 1000)); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'شروع استفاده از هوشان', + style: AppTextStyles.headline6 + .copyWith(color: Colors.white), + ), + const SizedBox(width: 8), + SvgPicture.asset('assets/icon/outline/arrow-left.svg') + ], + ), + ), + const SizedBox(height: 24), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _buildFeatureCard( + BuildContext context, { + required String icon, + required String title, + required String description, + required Color borderColor, + required Color titleColor, + required Color descColor, + }) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + border: Border.all(color: borderColor), + ), + child: Row( + children: [ + SvgPicture.asset( + icon, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: titleColor, + ), + ), + const SizedBox(height: 2), + Text( + description, + style: AppTextStyles.body6.copyWith( + color: descColor, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/screens/auth/login/bloc/login_bloc.dart b/lib/ui/screens/auth/login/bloc/login_bloc.dart new file mode 100644 index 0000000..c0055b4 --- /dev/null +++ b/lib/ui/screens/auth/login/bloc/login_bloc.dart @@ -0,0 +1,33 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/data/repository/auth_repository.dart'; +import 'package:hoshan/data/storage/shared_preferences_helper.dart'; + +part 'login_event.dart'; +part 'login_state.dart'; + +class LoginBloc extends Bloc { + LoginBloc() : super(LoginInitial()) { + on((event, emit) async { + if (event is LoginWithPassword) { + emit(LoginLoading()); + try { + final response = await AuthRepository.loginWithPassword( + event.username, event.password); + AuthTokenStorage.setToken(response.accessToken.toString()); + + emit(LoginSuccess()); + } on DioException catch (e) { + emit(LoginFail( + error: (e.response?.statusCode ?? 500) >= 500 + ? 'خطای سرور' + : e.response?.data['detail'])); + if (kDebugMode) { + print('Dio Error is $e'); + } + } + } + }); + } +} diff --git a/lib/ui/screens/auth/login/bloc/login_event.dart b/lib/ui/screens/auth/login/bloc/login_event.dart new file mode 100644 index 0000000..082bc0f --- /dev/null +++ b/lib/ui/screens/auth/login/bloc/login_event.dart @@ -0,0 +1,11 @@ +part of 'login_bloc.dart'; + +@immutable +sealed class LoginEvent {} + +class LoginWithPassword extends LoginEvent { + final String username; + final String password; + + LoginWithPassword({required this.username, required this.password}); +} diff --git a/lib/ui/screens/auth/login/bloc/login_state.dart b/lib/ui/screens/auth/login/bloc/login_state.dart new file mode 100644 index 0000000..69c988a --- /dev/null +++ b/lib/ui/screens/auth/login/bloc/login_state.dart @@ -0,0 +1,16 @@ +part of 'login_bloc.dart'; + +@immutable +sealed class LoginState {} + +final class LoginInitial extends LoginState {} + +final class LoginSuccess extends LoginState {} + +final class LoginFail extends LoginState { + final String error; + + LoginFail({required this.error}); +} + +final class LoginLoading extends LoginState {} diff --git a/lib/ui/screens/auth/login/login_screen.dart b/lib/ui/screens/auth/login/login_screen.dart new file mode 100644 index 0000000..352cefa --- /dev/null +++ b/lib/ui/screens/auth/login/login_screen.dart @@ -0,0 +1,193 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/data/model/ai/credit_model.dart'; + +import 'package:hoshan/ui/screens/auth/login/bloc/login_bloc.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; +import 'package:hoshan/ui/widgets/components/text/auth_text_field.dart'; + +class LoginScreen extends StatefulWidget { + const LoginScreen({super.key}); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + final TextEditingController username = TextEditingController(); + final TextEditingController password = TextEditingController(); + bool typeUser = false; + bool typePassword = false; + ValueNotifier passwodError = ValueNotifier(null); + ValueNotifier usernameError = ValueNotifier(null); + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) async { + if (state is LoginSuccess) { + await context.read().getUserInfo(); + context.read().changeCredit(CreditModel( + credit: UserInfoCubit.userInfoModel.credit, + freeCredit: UserInfoCubit.userInfoModel.freeCredit)); + context.go(Routes.home); + } + if (state is LoginFail) { + switch (state.error) { + case 'کاربر یافت نشد': + usernameError.value = state.error; + break; + case 'رمز عبور اشتباه است': + passwodError.value = state.error; + break; + default: + SnackBarManager(context, id: 'server-login').show( + message: 'خطا از طرف سرور لحظاتی دیگر دوباره امتحان کنید', + status: SnackBarStatus.error); + } + } + }, + builder: (context, state) { + return Column( + children: [ + Text( + '.نام کاربری و رمز عبور خود را وارد کنید', + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface), + ), + Padding( + padding: const EdgeInsets.all(18.0), + child: Directionality( + textDirection: TextDirection.rtl, + child: ValueListenableBuilder( + valueListenable: usernameError, + builder: (context, error, _) { + return AuthTextField( + hintText: 'مثال: Ali', + label: 'نام کاربری', + textInputAction: TextInputAction.next, + controller: username, + error: error != null + ? Row( + children: [ + Icon( + Icons.warning_amber_rounded, + color: AppColors.red.defaultShade, + size: 16, + ), + const SizedBox( + width: 8, + ), + Text( + error, + style: AppTextStyles.body5.copyWith( + color: AppColors.red.defaultShade), + ), + ], + ) + : null, + onChange: (val) { + usernameError.value = null; + setState(() { + typeUser = val.length > 2 ? true : false; + }); + }, + ); + })), + ), + Padding( + padding: const EdgeInsets.fromLTRB(18, 0, 18, 12), + child: Directionality( + textDirection: TextDirection.rtl, + child: ValueListenableBuilder( + valueListenable: passwodError, + builder: (context, error, _) { + return AuthTextField( + label: 'رمز عبور', + isPassword: true, + controller: password, + onChange: (val) { + passwodError.value = null; + + setState(() { + typePassword = val.length >= 8 ? true : false; + }); + }, + error: error != null + ? Row( + children: [ + Icon( + Icons.warning_amber_rounded, + color: AppColors.red.defaultShade, + size: 16, + ), + const SizedBox( + width: 8, + ), + Text( + error, + style: AppTextStyles.body5.copyWith( + color: AppColors.red.defaultShade), + ), + ], + ) + : null, + ); + })), + ), + // GestureDetector( + // onTap: () {}, + // child: SizedBox( + // width: MediaQuery.sizeOf(context).width, + // child: Padding( + // padding: const EdgeInsets.only(bottom: 18, right: 20), + // child: Text( + // 'رمز عبور خود را فراموش کردید؟', + // textAlign: TextAlign.right, + // style: AppTextStyles.body5.copyWith( + // fontWeight: FontWeight.bold, + // color: AppColors.primaryColor.defaultShade), + // ), + // ), + // ), + // ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: LoadingButton( + width: MediaQuery.sizeOf(context).width, + height: 48, + radius: 100, + loading: state is LoginLoading || state is LoginSuccess, + onPressed: typeUser && + typePassword && + usernameError.value == null && + passwodError.value == null + ? () { + context.read().add(LoginWithPassword( + username: username.text, + password: password.text)); + } + : null, + child: Text( + 'ورود', + style: AppTextStyles.body4.copyWith(color: Colors.white), + )), + ), + ], + ); + }, + ); + } +} diff --git a/lib/ui/screens/auth/on_boarding/on_boarding_page.dart b/lib/ui/screens/auth/on_boarding/on_boarding_page.dart new file mode 100644 index 0000000..907dd74 --- /dev/null +++ b/lib/ui/screens/auth/on_boarding/on_boarding_page.dart @@ -0,0 +1,256 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/data/model/on_boarding_slider.dart'; +import 'package:hoshan/data/storage/shared_preferences_helper.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/slider/custom_carousel_controller.dart'; +import 'package:smooth_page_indicator/smooth_page_indicator.dart'; + +class OnBoardingPage extends StatefulWidget { + final Function()? onFinish; + const OnBoardingPage({super.key, this.onFinish}); + + @override + State createState() => _OnBoardingPageState(); +} + +class _OnBoardingPageState extends State { + final CustomCarouselController _buttonCarouselController = + CustomCarouselController(); + final ValueNotifier isEnd = ValueNotifier(false); + + final List sliders = [ + OnBoardingSlider( + id: 0, + image: Assets.image.boardings.eins, + title: 'تصور کن یه مشاور همه چیزدان و حرفه‌ای، 24 ساعته در کنارته. ', + description: + 'امتحانش کن، هر سوالی داری ازش بپرس. هوشان دستیار هوش مصنوعی شماست و قراره بهت در هر زمینه‌ای کمک کنه. از فلسفه تا آشپزی، از کسب‌وکار تا تکنولوژی، از روانشناسی تا سلامت.'), + OnBoardingSlider( + id: 1, + image: Assets.image.boardings.zwei, + title: 'دستیار شخصی خودت رو بساز. ', + description: + 'می‌خوای یه همراه هوشمند داشته باشی که دقیقاً مطابق نیازهای تو باشه؟ هوشان بهت کمک می‌کنه تا یه دستیار اختصاصی طراحی کنی. حتی می‌تونی دستیارهاتو با دیگران به اشتراک بزاری.'), + OnBoardingSlider( + id: 2, + image: Assets.image.boardings.drei, + title: 'با چند کلمه، دنیایی از تصاویر شگفت‌انگیز بساز. ', + description: + 'ایده‌ای در ذهنت داری؟ یک توصیف ساده ازش بنویس تا هوشان اون را به تصویری زیبا و حیرت‌انگیز تبدیل کنه. از مناظر خیالی تا پرتره‌های هنری، از سبک‌های کلاسیک تا انیمه، همه چیز ممکنه.'), + OnBoardingSlider( + id: 3, + image: Assets.image.boardings.vier, + title: 'دوست داری موزیک‌های خودت رو بسازی؟', + description: + '، این کار شدنی و خیلی راحته؛ با هوشان آهنگ‌های منحصربه‌فرد خودت رو خلق کن! از ملودی‌های آرام و احساسی تا بیت‌های پرانرژی و هیجان‌انگیز، هوشان بهت کمک می‌کنه تا رویاهای موسیقیایی‌ات رو به واقعیت تبدیل کنی.'), + OnBoardingSlider( + id: 4, + image: Assets.image.boardings.funf, + title: 'ذهن خلاق تو، فیلم بعدی رو می‌سازه. ', + description: + 'ایده‌ای در ذهنت داری؟ یک سناریوی جذاب بنویس و خیلی ساده برای هوشان توضیحش بده تا اون را به ویدیویی شگفت‌انگیز تبدیل کنه. از فیلم‌های علمی-تخیلی تا درام‌های احساسی، فیلم هر داستانی که تصور کنی رو می‌تونی بسازی.'), + OnBoardingSlider( + id: 5, + image: Assets.image.boardings.sechs, + title: 'دلت می‌خواد پای صحبت بزرگون بشینی؟ ', + description: + 'تصور کن که می‌تونستی با نابغه‌ها، فیلسوف‌ها، دانشمندا و رهبران بزرگ دنیا چت کنی. از سقراط تا استیو جابز، از مولانا تا انیشتین یا هر آدم خفن دیگه‌ای. ازش چی می‌پرسیدی؟') + // OnBoardingSlider( + // id: 2, + // image: Assets.image.boardings.board3, + // title: + // 'در بخش گفتگوی هوشان می‌توانید در مورد هر چیزی از نوشتن پرامپت تا ایده‌های نو در حوزه هوش مصنوعی با دیگران گفتگو کنید'), + ]; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final CarouselOptions carouselOptions = CarouselOptions( + viewportFraction: 1, + initialPage: 0, + disableCenter: true, + enableInfiniteScroll: false, + reverse: false, + autoPlay: Responsive(context).isMobile() ? false : true, + autoPlayCurve: Curves.fastOutSlowIn, + enlargeCenterPage: true, + enlargeFactor: 0.5, + onPageChanged: (index, _) {}, + scrollDirection: Axis.horizontal, + height: MediaQuery.sizeOf(context).height, + ); + return Scaffold( + body: Column( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Stack( + children: [ + CarouselSlider.builder( + carouselController: _buttonCarouselController, + itemCount: sliders.length, + itemBuilder: (context, index, realIndex) { + return sliderView(index); + }, + options: carouselOptions), + Positioned.fill( + child: Container( + alignment: const Alignment(0, 0.35), + child: sliderIndicator(), + )) + ], + ), + ), + ), + if (Responsive(context).isMobile()) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 32), + child: ValueListenableBuilder( + valueListenable: isEnd, + builder: (context, value, child) { + return Row( + children: [ + if (!value) + Row( + children: [ + ElevatedButton( + onPressed: () { + widget.onFinish?.call(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.gray[ + context.read().isDark() + ? 900 + : 400], + elevation: 0), + child: Text('انصراف', + style: AppTextStyles.body4 + .copyWith(color: Colors.white))), + const SizedBox( + width: 8, + ), + ], + ), + Expanded( + child: ElevatedButton( + onPressed: () async { + if (value) { + await OnBoardingStorage.setAsSeen(); + widget.onFinish?.call(); + return; + } + _buttonCarouselController.nextPage(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: + AppColors.secondryColor.defaultShade), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + value ? 'بزن بریم' : 'بعدی', + style: AppTextStyles.body4 + .copyWith(color: Colors.white), + ), + Assets.icon.outline.arrowRight + .svg(color: Colors.white) + ], + )), + ), + ], + ); + }, + ), + ) + ], + ), + ); + } + + Widget sliderIndicator() { + return FutureBuilder( + future: _buttonCarouselController.onReady, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + _buttonCarouselController.state!.pageController!.addListener(() { + if (_buttonCarouselController.state!.pageController!.page! > + (sliders.length - 1.5)) { + isEnd.value = true; + } else { + isEnd.value = false; + } + }); + return SmoothPageIndicator( + controller: _buttonCarouselController.state!.pageController!, + count: sliders.length, + effect: ExpandingDotsEffect( + dotWidth: 8, + dotHeight: 8, + activeDotColor: AppColors.secondryColor.defaultShade, + dotColor: AppColors.secondryColor[300])); + } + return const SizedBox(); + }); + } + + Widget sliderView(int index) { + return Padding( + padding: EdgeInsets.symmetric( + vertical: Responsive(context).isDesktop() ? 64 : 46.0), + child: + Column(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Flexible( + flex: 2, + child: sliders[index] + .image + .image(width: double.infinity, height: double.infinity), + ), + Flexible( + child: Column( + children: [ + const SizedBox( + height: 48, + ), + Flexible( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text.rich( + TextSpan( + text: sliders[index].title, + children: [ + TextSpan( + text: sliders[index].description, + style: AppTextStyles.body3.copyWith( + color: + Theme.of(context).colorScheme.onSurface)) + ], + style: AppTextStyles.body3.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold)), + textAlign: TextAlign.justify, + textDirection: TextDirection.rtl, + // overflow: TextOverflow.ellipsis, + // softWrap: true, + ), + ), + ) + ], + ), + ) + ]), + ); + } +} diff --git a/lib/ui/screens/auth/register/bloc/register_bloc.dart b/lib/ui/screens/auth/register/bloc/register_bloc.dart new file mode 100644 index 0000000..ae9e5e3 --- /dev/null +++ b/lib/ui/screens/auth/register/bloc/register_bloc.dart @@ -0,0 +1,29 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/data/repository/auth_repository.dart'; + +part 'register_event.dart'; +part 'register_state.dart'; + +class RegisterBloc extends Bloc { + RegisterBloc() : super(RegisterInitial()) { + on((event, emit) async { + if (event is LoginUser) { + emit(RegisterLoading()); + try { + final response = await AuthRepository.sendOtp(event.phoneNumber); + + emit(RegisterSuccess(isNew: response)); + } on DioException catch (e) { + emit(RegisterFail( + error: e.response?.data['detail'], + statusCode: e.response!.statusCode ?? 500)); + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } + }); + } +} diff --git a/lib/ui/screens/auth/register/bloc/register_event.dart b/lib/ui/screens/auth/register/bloc/register_event.dart new file mode 100644 index 0000000..7c582e7 --- /dev/null +++ b/lib/ui/screens/auth/register/bloc/register_event.dart @@ -0,0 +1,9 @@ +part of 'register_bloc.dart'; + +sealed class RegisterEvent {} + +class LoginUser extends RegisterEvent { + final String phoneNumber; + + LoginUser({required this.phoneNumber}); +} diff --git a/lib/ui/screens/auth/register/bloc/register_state.dart b/lib/ui/screens/auth/register/bloc/register_state.dart new file mode 100644 index 0000000..0f88b9f --- /dev/null +++ b/lib/ui/screens/auth/register/bloc/register_state.dart @@ -0,0 +1,20 @@ +part of 'register_bloc.dart'; + +sealed class RegisterState {} + +final class RegisterInitial extends RegisterState {} + +final class RegisterSuccess extends RegisterState { + final bool isNew; + + RegisterSuccess({required this.isNew}); +} + +final class RegisterFail extends RegisterState { + final String? error; + final int statusCode; + + RegisterFail({required this.error, required this.statusCode}); +} + +final class RegisterLoading extends RegisterState {} diff --git a/lib/ui/screens/auth/register/register_screen.dart b/lib/ui/screens/auth/register/register_screen.dart new file mode 100644 index 0000000..a27ff2e --- /dev/null +++ b/lib/ui/screens/auth/register/register_screen.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/data/model/auth/auth_screens_enum.dart'; +import 'package:hoshan/ui/screens/auth/cubit/auth_screens_cubit.dart'; +import 'package:hoshan/ui/screens/auth/register/bloc/register_bloc.dart'; +import 'package:hoshan/ui/screens/auth/verification/verification_screen.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/text/mobile_number_text_field.dart'; +import 'package:persian_number_utility/persian_number_utility.dart'; +import 'package:string_validator/string_validator.dart'; + +class RegisterScreen extends StatefulWidget { + const RegisterScreen({super.key}); + + @override + State createState() => _RegisterScreenState(); +} + +class _RegisterScreenState extends State { + final TextEditingController number = TextEditingController(); + final ValueNotifier error = ValueNotifier(false); + bool canSend = false; + @override + Widget build(BuildContext context) { + return Column( + children: [ + // Text( + // '.شماره همراه یا ایمیل خود را وارد کنید', + // style: AppTextStyles.body4.copyWith( + // fontWeight: FontWeight.bold, + // color: Theme.of(context).colorScheme.onSurface), + // ), + BlocConsumer( + listener: (context, state) { + if (state is RegisterSuccess) { + context.read().username = number.text; + context.read().isNew = state.isNew; + context + .read() + .changeState(AuthScreens.verification); + } + if (state is RegisterFail) { + if (state.statusCode == 429 && state.error != null) { + seconds.value = + int.tryParse(state.error!.replaceAll(RegExp(r'\D'), '')) ?? + 120; + context.read().username = number.text; + + context + .read() + .changeState(AuthScreens.verification); + error.value = false; + + return; + } + error.value = true; + } + }, + builder: (context, state) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(18.0), + child: Directionality( + textDirection: TextDirection.rtl, + child: ValueListenableBuilder( + valueListenable: error, + builder: (context, errVal, _) { + return MobileNumberTextField( + controller: number, + onChange: (val) { + setState(() { + val = val.toEnglishDigit(); + canSend = val.length == 11 && isNumeric(val); + }); + }, + // maxLength: 11, + label: 'شماره همراه', + // suffix: const Icon(CupertinoIcons.phone), + error: errVal + ? Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Icon( + Icons.warning_amber_rounded, + color: AppColors.red.defaultShade, + size: 16, + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + (state as RegisterFail).error ?? '', + style: AppTextStyles.body5.copyWith( + color: + AppColors.red.defaultShade), + ), + ), + ], + ) + : null, + ); + }), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: LoadingButton( + width: MediaQuery.sizeOf(context).width, + height: 48, + radius: 100, + loading: state is RegisterLoading, + onPressed: canSend + ? () { + context.read().add(LoginUser( + phoneNumber: number.text.toEnglishDigit())); + } + : null, + child: Text( + 'ارسال کد', + style: AppTextStyles.body4.copyWith( + color: canSend + ? Colors.white + : Color.fromARGB(255, 112, 112, 110)), + )), + ), + ], + ); + }, + ), + ], + ); + } +} diff --git a/lib/ui/screens/auth/verification/bloc/verification_bloc.dart b/lib/ui/screens/auth/verification/bloc/verification_bloc.dart new file mode 100644 index 0000000..247f505 --- /dev/null +++ b/lib/ui/screens/auth/verification/bloc/verification_bloc.dart @@ -0,0 +1,30 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/data/repository/auth_repository.dart'; +import 'package:hoshan/data/storage/shared_preferences_helper.dart'; + +part 'verification_event.dart'; +part 'verification_state.dart'; + +class VerificationBloc extends Bloc { + VerificationBloc() : super(VerificationInitial()) { + on((event, emit) async { + if (event is LoginWithOTP) { + emit(VerificationLoading()); + try { + final response = + await AuthRepository.loginWithOTP(event.number, event.otp); + AuthTokenStorage.setToken(response.accessToken.toString()); + emit(VerificationSuccess(isNew: event.isNew)); + } on DioException catch (e) { + emit(VerificationFail( + errorServer: (e.response?.statusCode ?? 500) >= 500)); + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } + }); + } +} diff --git a/lib/ui/screens/auth/verification/bloc/verification_event.dart b/lib/ui/screens/auth/verification/bloc/verification_event.dart new file mode 100644 index 0000000..50a17f2 --- /dev/null +++ b/lib/ui/screens/auth/verification/bloc/verification_event.dart @@ -0,0 +1,12 @@ +part of 'verification_bloc.dart'; + +@immutable +sealed class VerificationEvent {} + +class LoginWithOTP extends VerificationEvent { + final String number; + final String otp; + final bool isNew; + + LoginWithOTP({required this.number, required this.otp, required this.isNew}); +} diff --git a/lib/ui/screens/auth/verification/bloc/verification_state.dart b/lib/ui/screens/auth/verification/bloc/verification_state.dart new file mode 100644 index 0000000..508f277 --- /dev/null +++ b/lib/ui/screens/auth/verification/bloc/verification_state.dart @@ -0,0 +1,20 @@ +part of 'verification_bloc.dart'; + +@immutable +sealed class VerificationState {} + +final class VerificationInitial extends VerificationState {} + +final class VerificationSuccess extends VerificationState { + final bool isNew; + + VerificationSuccess({required this.isNew}); +} + +final class VerificationFail extends VerificationState { + final bool errorServer; + + VerificationFail({this.errorServer = false}); +} + +final class VerificationLoading extends VerificationState {} diff --git a/lib/ui/screens/auth/verification/sms_retriever_impl.dart b/lib/ui/screens/auth/verification/sms_retriever_impl.dart new file mode 100644 index 0000000..b43a2b5 --- /dev/null +++ b/lib/ui/screens/auth/verification/sms_retriever_impl.dart @@ -0,0 +1,25 @@ +import 'package:pinput/pinput.dart'; +import 'package:smart_auth/smart_auth.dart'; + +class SmsRetrieverImpl implements SmsRetriever { + const SmsRetrieverImpl(this.smartAuth); + + final SmartAuth smartAuth; + + @override + Future dispose() { + return smartAuth.removeUserConsentApiListener(); + } + + @override + Future getSmsCode() async { + final res = await smartAuth.getSmsWithUserConsentApi(); + if (res.hasData && res.data != null && res.data!.sms.contains('هوشان')) { + return res.requireData.code!; + } + return null; + } + + @override + bool get listenForMultipleSms => true; +} diff --git a/lib/ui/screens/auth/verification/verification_screen.dart b/lib/ui/screens/auth/verification/verification_screen.dart new file mode 100644 index 0000000..e27d92c --- /dev/null +++ b/lib/ui/screens/auth/verification/verification_screen.dart @@ -0,0 +1,286 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/core/utils/date_time.dart'; +import 'package:hoshan/data/model/auth/auth_screens_enum.dart'; +import 'package:hoshan/ui/screens/auth/cubit/auth_screens_cubit.dart'; +import 'package:hoshan/ui/screens/auth/register/bloc/register_bloc.dart'; +import 'package:hoshan/ui/screens/auth/verification/bloc/verification_bloc.dart'; +import 'package:hoshan/ui/screens/auth/verification/sms_retriever_impl.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:pinput/pinput.dart'; +import 'package:smart_auth/smart_auth.dart'; +import 'package:string_validator/string_validator.dart'; + +ValueNotifier seconds = ValueNotifier(120); + +class VerificationScreen extends StatefulWidget { + const VerificationScreen({super.key}); + + @override + State createState() => _VerificationScreenState(); +} + +class _VerificationScreenState extends State { + ValueNotifier error = ValueNotifier(false); + ValueNotifier readOnly = ValueNotifier(false); + Timer? _timer; + late final defaultPinTheme = PinTheme( + textStyle: AppTextStyles.headline5 + .copyWith(color: Theme.of(context).colorScheme.primary), + width: 56, + height: 56, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.gray.defaultShade))); + + final errorPinTheme = PinTheme( + textStyle: + AppTextStyles.headline5.copyWith(color: AppColors.red.defaultShade), + width: 56, + height: 56, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.red.defaultShade))); + + void startTimer() { + _timer = Timer.periodic(const Duration(seconds: 1), (Timer timer) { + seconds.value = seconds.value - 1; + if (seconds.value == 0) { + timer.cancel(); + error.value = true; + readOnly.value = true; + return; + } + }); + } + + @override + void initState() { + startTimer(); + super.initState(); + } + + @override + void dispose() { + super.dispose(); + + _timer?.cancel(); + } + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) async { + if (state is VerificationLoading) { + readOnly.value = true; + } else { + readOnly.value = false; + } + if (state is VerificationFail) { + error.value = true; + } else if (state is VerificationSuccess) { + if (state.isNew) { + context.read().changeState(AuthScreens.code); + } else { + await context.read().getUserInfo(); + context.go(Routes.giftCredit); + } + } + }, + builder: (context, state) { + return ValueListenableBuilder( + valueListenable: readOnly, + builder: (context, readOnlyVal, _) { + return Column( + children: [ + Text( + 'کد تایید شش رقمی به ${context.read().username.isEmail ? 'ایمیل شما' : 'شماره شما'} ارسال شد.', + textDirection: TextDirection.rtl, + style: AppTextStyles.body5.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () => context + .read() + .changeState(AuthScreens.mobile), + child: Text( + 'تغییر ${context.read().username.isEmail ? 'ایمیل' : 'شماره همراه'} ', + style: AppTextStyles.body5.copyWith( + color: Theme.of(context).colorScheme.primary), + ), + ), + const SizedBox( + width: 4, + ), + Icon( + Icons.edit, + size: 16, + color: Theme.of(context).colorScheme.primary, + ) + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 24.0, horizontal: 16), + child: ValueListenableBuilder( + valueListenable: error, + builder: (context, errVal, _) { + return Column( + children: [ + Pinput( + smsRetriever: !kIsWeb && !Platform.isIOS + ? SmsRetrieverImpl(SmartAuth.instance) + : null, + readOnly: readOnlyVal, + forceErrorState: errVal, + defaultPinTheme: defaultPinTheme, + focusedPinTheme: + defaultPinTheme.copyBorderWith( + border: Border.all( + color: Theme.of(context) + .colorScheme + .primary)), + submittedPinTheme: + defaultPinTheme.copyBorderWith( + border: Border.all( + color: Theme.of(context) + .colorScheme + .primary)), + errorPinTheme: errorPinTheme, + keyboardType: TextInputType.number, + length: 6, + autofocus: true, + closeKeyboardWhenCompleted: true, + onChanged: (value) { + error.value = false; + }, + onCompleted: (String value) { + final number = context + .read() + .username; + final isNew = + context.read().isNew; + context.read().add( + LoginWithOTP( + number: number, + otp: value, + isNew: isNew)); + }, + ), + const SizedBox( + height: 12, + ), + if (errVal) + Text( + seconds.value == 0 + ? 'دوباره تلاش کنید' + : context.read().state + is VerificationFail && + (state as VerificationFail) + .errorServer + ? "خطا از طرف سرور" + : '!کد وارد شده اشتباه است', + style: errorPinTheme.textStyle, + ), + const SizedBox( + height: 12, + ), + if (state is VerificationLoading || + state is VerificationSuccess) + SpinKitThreeBounce( + color: + Theme.of(context).colorScheme.primary, + size: 32, + ) + ], + ); + })), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 18.0, horizontal: 16), + child: readOnlyVal && state is! VerificationLoading + ? BlocConsumer( + listener: (context, state) { + if (state is RegisterSuccess) { + readOnly.value = false; + error.value = false; + seconds.value = 120; + startTimer(); + } + }, + builder: (context, state) { + return LoadingButton( + width: MediaQuery.sizeOf(context).width, + height: 48, + radius: 100, + loading: state is RegisterLoading, + onPressed: () async { + context.read().add(LoginUser( + phoneNumber: context + .read() + .username)); + }, + child: Text( + 'ارسال مجدد کد', + style: AppTextStyles.body4 + .copyWith(color: Colors.white), + )); + }, + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'تا دریافت مجدد کد', + style: AppTextStyles.body4.copyWith( + color: + Theme.of(context).colorScheme.primary), + ), + ValueListenableBuilder( + valueListenable: seconds, + builder: (context, secVal, _) { + return Text( + ' ${DateTimeUtils.getTimeFromDuration(secVal)}', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .primary)); + }), + const SizedBox( + width: 4, + ), + Icon( + CupertinoIcons.clock, + size: 16, + color: Theme.of(context).colorScheme.primary, + ) + ], + ), + ) + ], + ); + }); + }, + ); + } +} diff --git a/lib/ui/screens/chat/bloc/messages_bloc.dart b/lib/ui/screens/chat/bloc/messages_bloc.dart new file mode 100644 index 0000000..ac02740 --- /dev/null +++ b/lib/ui/screens/chat/bloc/messages_bloc.dart @@ -0,0 +1,127 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/ai/messages_model.dart'; +import 'package:hoshan/data/repository/chatbot_repository.dart'; + +part 'messages_event.dart'; +part 'messages_state.dart'; + +class MessagesBloc extends Bloc { + MessagesBloc() : super(MessagesInitial()) { + on((event, emit) async { + if (event is GetallMessages) { + emit(MessagesLoading()); + try { + final response = + await ChatbotRepository.getMessages(id: event.chatId); + final updatedList = List.from(response.messages!); + for (var i = 0; i < updatedList.length; i++) { + try { + updatedList[i].query = updatedList[i] + .content + ?.firstWhere( + (element) => element.type == 'text', + ) + .text; + if (updatedList[i].fromBot! && i > 0) { + if (!updatedList[i - 1].fromBot!) { + updatedList[i].query = updatedList[i - 1] + .content + ?.firstWhere( + (element) => element.type == 'text', + ) + .text; + } + } + } catch (e) { + if (kDebugMode) { + print('Query Error is: $e'); + } + } + } + if (updatedList.isEmpty) { + emit(MessagesEmpty()); + } else { + emit(MessagesSuccess( + isGetAll: true, + messages: updatedList)); // Copy the current state + } + } on DioException catch (e) { + emit(MessagesFail()); + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } + if (event is AddMessage) { + final updatedList = List.from(state.messages); + updatedList.add(event.message); + emit(MessagesSuccess(messages: updatedList)); // Copy the current state + } + + if (event is RemoveInError) { + final updatedList = List.from(state.messages); + updatedList.removeLast(); + updatedList.removeLast(); + + emit(MessagesSuccess(messages: updatedList)); // Copy the current state + } + + if (event is ChangeMessage) { + final updatedList = List.from(state.messages); + + final index = updatedList.indexOf(event.oldMessage); + updatedList[index] = event.newMessage; + + emit(MessagesSuccess(messages: updatedList)); // Copy the current state + } + + if (event is DeleteMessage) { + try { + if (event.chatId != null) { + await ChatbotRepository.deleteMessage( + chatId: event.chatId!, messageId: event.message.id!); + } + + final updatedList = List.from(state.messages); + + updatedList.remove(event.message); + if (updatedList.isEmpty) { + emit(MessagesEmpty()); + } else { + emit(MessagesSuccess(messages: updatedList)); + } + } catch (e) { + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } + if (event is DeleteMessageWithId) { + try { + final updatedList = List.from(state.messages); + + updatedList.removeWhere( + (element) { + return element.id == event.messageId; + }, + ); + if (updatedList.isEmpty) { + emit(MessagesEmpty()); + } else { + emit(MessagesSuccess(messages: updatedList)); + } + } catch (e) { + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } + if (event is ResetMessages) { + emit(MessagesInitial()); + } + }); + } +} diff --git a/lib/ui/screens/chat/bloc/messages_event.dart b/lib/ui/screens/chat/bloc/messages_event.dart new file mode 100644 index 0000000..b6d179c --- /dev/null +++ b/lib/ui/screens/chat/bloc/messages_event.dart @@ -0,0 +1,44 @@ +part of 'messages_bloc.dart'; + +sealed class MessagesEvent extends Equatable { + const MessagesEvent(); + + @override + List get props => []; +} + +class GetallMessages extends MessagesEvent { + final int chatId; + + const GetallMessages({required this.chatId}); +} + +class AddMessage extends MessagesEvent { + final Messages message; + + const AddMessage({required this.message}); +} + +class ChangeMessage extends MessagesEvent { + final Messages oldMessage; + final Messages newMessage; + + const ChangeMessage({required this.oldMessage, required this.newMessage}); +} + +class DeleteMessage extends MessagesEvent { + final int? chatId; + final Messages message; + + const DeleteMessage({required this.chatId, required this.message}); +} + +class DeleteMessageWithId extends MessagesEvent { + final String messageId; + + const DeleteMessageWithId({required this.messageId}); +} + +class RemoveInError extends MessagesEvent {} + +class ResetMessages extends MessagesEvent {} diff --git a/lib/ui/screens/chat/bloc/messages_state.dart b/lib/ui/screens/chat/bloc/messages_state.dart new file mode 100644 index 0000000..e9fa5f0 --- /dev/null +++ b/lib/ui/screens/chat/bloc/messages_state.dart @@ -0,0 +1,23 @@ +part of 'messages_bloc.dart'; + +sealed class MessagesState extends Equatable { + final List messages; + + const MessagesState({this.messages = const []}); + + @override + List get props => [messages]; +} + +final class MessagesInitial extends MessagesState {} + +final class MessagesLoading extends MessagesState {} + +final class MessagesFail extends MessagesState {} + +final class MessagesSuccess extends MessagesState { + final bool isGetAll; + const MessagesSuccess({required super.messages, this.isGetAll = false}); +} + +final class MessagesEmpty extends MessagesState {} diff --git a/lib/ui/screens/chat/bloc/related_questions_bloc.dart b/lib/ui/screens/chat/bloc/related_questions_bloc.dart new file mode 100644 index 0000000..11c3b40 --- /dev/null +++ b/lib/ui/screens/chat/bloc/related_questions_bloc.dart @@ -0,0 +1,34 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/data/model/ai/related_questions_model.dart'; +import 'package:hoshan/data/repository/chatbot_repository.dart'; + +part 'related_questions_event.dart'; +part 'related_questions_state.dart'; + +class RelatedQuestionsBloc + extends Bloc { + RelatedQuestionsBloc() : super(RelatedQuestionsInitial()) { + on((event, emit) async { + if (event is GetAllRelatedQuestions) { + emit(RelatedQuestionsLoading()); + try { + final response = await ChatbotRepository.getRelatedQuestions( + chatId: event.chatId, + messageId: event.messageId, + content: event.content); + emit(RelatedQuestionsSuccess(relatedQuestionsModel: response)); + } catch (e) { + emit(RelatedQuestionsFail()); + if (kDebugMode) { + print("Dio Error: $e"); + } + } + } + if (event is ClearAllRelatedQuestions) { + emit(RelatedQuestionsFail()); + } + }); + } +} diff --git a/lib/ui/screens/chat/bloc/related_questions_event.dart b/lib/ui/screens/chat/bloc/related_questions_event.dart new file mode 100644 index 0000000..f980a9a --- /dev/null +++ b/lib/ui/screens/chat/bloc/related_questions_event.dart @@ -0,0 +1,19 @@ +part of 'related_questions_bloc.dart'; + +sealed class RelatedQuestionsEvent extends Equatable { + const RelatedQuestionsEvent(); + + @override + List get props => []; +} + +class GetAllRelatedQuestions extends RelatedQuestionsEvent { + final int chatId; + final String messageId; + final String content; + + const GetAllRelatedQuestions( + {required this.chatId, required this.messageId, required this.content}); +} + +class ClearAllRelatedQuestions extends RelatedQuestionsEvent {} diff --git a/lib/ui/screens/chat/bloc/related_questions_state.dart b/lib/ui/screens/chat/bloc/related_questions_state.dart new file mode 100644 index 0000000..4d9b1d3 --- /dev/null +++ b/lib/ui/screens/chat/bloc/related_questions_state.dart @@ -0,0 +1,20 @@ +part of 'related_questions_bloc.dart'; + +sealed class RelatedQuestionsState extends Equatable { + const RelatedQuestionsState(); + + @override + List get props => []; +} + +final class RelatedQuestionsInitial extends RelatedQuestionsState {} + +final class RelatedQuestionsLoading extends RelatedQuestionsState {} + +final class RelatedQuestionsSuccess extends RelatedQuestionsState { + final RelatedQuestionsModel relatedQuestionsModel; + + const RelatedQuestionsSuccess({required this.relatedQuestionsModel}); +} + +final class RelatedQuestionsFail extends RelatedQuestionsState {} diff --git a/lib/ui/screens/chat/chat_page.dart b/lib/ui/screens/chat/chat_page.dart new file mode 100644 index 0000000..9ff7da6 --- /dev/null +++ b/lib/ui/screens/chat/chat_page.dart @@ -0,0 +1,2260 @@ +// ignore_for_file: deprecated_member_use_from_same_package, use_build_context_synchronously + +import 'dart:math'; + +import 'package:cross_file/cross_file.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/gen/my_flutter_app_icons.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/core/services/file_manager/download_file_services.dart'; +import 'package:hoshan/core/services/file_manager/pick_file_services.dart'; +import 'package:hoshan/core/utils/date_time.dart'; +import 'package:hoshan/core/utils/strings.dart'; +import 'package:hoshan/data/model/ai/chats_history_model.dart'; +import 'package:hoshan/data/model/ai/credit_model.dart'; +import 'package:hoshan/data/model/ai/messages_model.dart'; +import 'package:hoshan/data/model/ai/send_message_model.dart'; +import 'package:hoshan/data/model/chat_args.dart'; +import 'package:hoshan/data/model/empty_states_enum.dart'; +import 'package:hoshan/ui/screens/chat/bloc/messages_bloc.dart'; +import 'package:hoshan/ui/screens/chat/bloc/related_questions_bloc.dart'; +import 'package:hoshan/ui/screens/chat/cubit/receive_message_cubit.dart'; +import 'package:hoshan/ui/screens/library/bloc/chats_history_bloc.dart'; +import 'package:hoshan/ui/screens/library/library_screen.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/animations/animated_visibility.dart'; +import 'package:hoshan/ui/widgets/components/audio/player.dart'; +import 'package:hoshan/ui/widgets/components/audio/recorder.dart'; +import 'package:hoshan/ui/widgets/components/button/circle_icon_btn.dart'; +import 'package:hoshan/ui/screens/chat/cubit/like_message_cubit.dart'; +import 'package:hoshan/ui/widgets/components/dialog/bottom_sheets.dart'; +import 'package:hoshan/ui/widgets/components/dialog/dialog_handler.dart'; +import 'package:hoshan/ui/widgets/components/dropdown/hint_tooltip.dart'; +import 'package:hoshan/ui/widgets/components/dropdown/more_popup_menu.dart'; +import 'package:hoshan/ui/widgets/components/image/custome_image.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; +import 'package:hoshan/ui/widgets/components/text/default_markdown_text.dart'; +import 'package:hoshan/ui/widgets/components/video/video_thumbnail.dart'; +import 'package:hoshan/ui/widgets/sections/empty/empty_states.dart'; +import 'package:hoshan/ui/widgets/sections/loading/chat_screen_placeholder.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; + +class ChatPage extends StatefulWidget { + final ChatArgs chatArgs; + const ChatPage({super.key, required this.chatArgs}); + + @override + State createState() => _ChatPageState(); +} + +class _ChatPageState extends State { + late int? chatId = widget.chatArgs.chatId; + late final bot = widget.chatArgs.bot; + late final ValueNotifier visibleAttach = + ValueNotifier(widget.chatArgs.bot.attachment == 3); + final ValueNotifier showRecorder = ValueNotifier(false); + final ValueNotifier isGhost = ValueNotifier(false); + final ValueNotifier recording = ValueNotifier(false); + final ValueNotifier refreshQuestions = ValueNotifier(true); + final ValueNotifier webSearch = ValueNotifier(false); + final ValueNotifier showInfo = ValueNotifier(false); + final ValueNotifier selectedFile = ValueNotifier(null); + final TextEditingController messageText = TextEditingController(); + ValueNotifier maxLines = ValueNotifier(5); + + void sendRequest( + {required final String? message, + final bool retry = false, + final XFile? file, + final bool withOutNewMessage = false}) { + final creditState = CreditModel( + credit: UserInfoCubit.userInfoModel.credit ?? 0, + freeCredit: UserInfoCubit.userInfoModel.freeCredit ?? 0); + int credit = (creditState.freeCredit ?? 0) + (creditState.credit ?? 0); + if (credit < widget.chatArgs.bot.cost!) { + DialogHandler(context: context).showUpgradeCredit(); + messageText.text = message ?? ''; + + return; + } + + if (!withOutNewMessage) { + context.read().add(AddMessage( + message: Messages( + role: 'human', + id: 'hero', + content: [ + if (message != null && message.isNotEmpty) + Content(type: 'text', text: message) + ], + query: message, + file: file, + retry: retry))); + } + + context.read().execute( + request: SendMessageModel( + botId: bot.id, + file: file, + id: chatId, + messageId: 'hero', + query: message, + ghost: isGhost.value, + tool: bot.tool, + webSearch: webSearch.value, + retry: retry)); + if (widget.chatArgs.bot.attachment != 3 && refreshQuestions.value) { + context.read().add(ClearAllRelatedQuestions()); + } + + selectedFile.value = null; + showRecorder.value = false; + messageText.clear(); + } + + void _popUpMenu( + Messages message, GlobalKey> containerKey) { + String copyText = ''; + List urls = []; + + if (message.content != null && message.content!.isNotEmpty) { + for (var content in message.content!) { + if (content.imageUrl != null) { + if (content.imageUrl!.url != null) { + urls.add(content.imageUrl!.url!); + } + copyText += content.imageUrl!.query ?? ''; + copyText += ' '; + } + if (content.audioUrl != null) { + if (content.audioUrl!.url != null) { + urls.add(content.audioUrl!.url!); + } + copyText += content.audioUrl!.query ?? ''; + copyText += ' '; + } + if (content.pdfUrl != null) { + if (content.pdfUrl!.url != null) { + urls.add(content.pdfUrl!.url!); + } + copyText += content.pdfUrl!.query ?? ''; + copyText += ' '; + } + if (content.pdfUrl == null && + content.imageUrl == null && + content.audioUrl == null) { + copyText += content.text ?? ''; + copyText += ' '; + } + } + } + final items = [ + PopUpMenuItemModel( + popupMenuItem: PopupMenuItem( + value: 0, + child: MorePopupMenuHandler.morePopUpItem( + color: message.fromBot! + ? Theme.of(context).colorScheme.primary + : Colors.white, + icon: Assets.icon.outline.trash, + title: 'حذف')), + click: () { + try { + context + .read() + .add(DeleteMessage(chatId: chatId!, message: message)); + } catch (e) { + if (kDebugMode) { + print("Error when delete message: $e"); + } + } + }, + ), + if ((bot.deleted != null && !bot.deleted!)) + if (!message.fromBot!) + PopUpMenuItemModel( + popupMenuItem: PopupMenuItem( + value: 1, + child: MorePopupMenuHandler.morePopUpItem( + color: message.fromBot! + ? Theme.of(context).colorScheme.primary + : Colors.white, + icon: Assets.icon.outline.bitcoinRefresh, + title: 'دوباره بپرس')), + click: () { + if (message.content != null && + message.content! + .firstWhere( + (element) => element.type == 'text', + ) + .text != + null) { + refreshQuestions.value = false; + sendRequest( + file: message.file, + message: message.content! + .firstWhere( + (element) => element.type == 'text', + ) + .text!, + retry: true); + } + }, + ), + if (copyText.replaceAll(' ', '').isNotEmpty) + PopUpMenuItemModel( + popupMenuItem: PopupMenuItem( + value: 2, + child: MorePopupMenuHandler.morePopUpItem( + color: message.fromBot! + ? Theme.of(context).colorScheme.primary + : Colors.white, + icon: Assets.icon.outline.copy, + title: 'کپی')), + click: () async { + await Clipboard.setData(ClipboardData(text: copyText)); + if (mounted) { + SnackBarManager(context, id: 'Copy').show( + status: SnackBarStatus.success, + message: 'پیام با موفقیت کپی شد 😃', + ); + } + }, + ), + ]; + MorePopupMenuHandler(context: context).showMorePopupMenu( + right: message.fromBot!, + color: message.error! + ? AppColors.red.defaultShade + : message.fromBot! + ? Theme.of(context).colorScheme.surface + : AppColors.primaryColor.defaultShade, + containerKey: containerKey, + items: [ + ...items, + if (urls.isNotEmpty) + for (var i = 0; i < urls.length; i++) + PopUpMenuItemModel( + popupMenuItem: PopupMenuItem( + value: i + items.length, + child: MorePopupMenuHandler.morePopUpItem( + color: message.fromBot! + ? Theme.of(context).colorScheme.primary + : Colors.white, + icon: Assets.icon.outline.download, + title: + 'دانلود ${urls[i].isVideo() ? "ویدیو" : urls[i].isImage() ? 'عکس' : urls[i].isAudio() ? 'فایل صوتی' : 'فایل'}')), + click: () async { + DownloadFileService.getFile(url: urls[i]).then((value) { + SnackBarManager(context).show( + message: 'فایل با موفقیت در پوشه Downloads نشست.', + status: SnackBarStatus.success); + }); + }, + ), + ]); + } + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().add(RestartChatsHistory()); + + context.read().add( + GetAllChats(type: widget.chatArgs.isPerson ? 'character' : 'llm')); + }); + } + + late double maxWidthDesktop; + @override + Widget build(BuildContext context) { + return Theme( + data: Theme.of(context).copyWith( + bottomSheetTheme: const BottomSheetThemeData( + surfaceTintColor: Colors.transparent, + backgroundColor: Colors.transparent)), + child: Scaffold( + drawer: Drawer( + shape: const BeveledRectangleBorder(borderRadius: BorderRadius.zero), + child: LibraryScreen( + type: widget.chatArgs.isPerson ? 'character' : 'llm', + onTap: (chat) async { + context.push(Routes.chat, + extra: ChatArgs( + bot: chat.bot!, + chatId: chat.id, + isPerson: widget.chatArgs.isPerson)); + }, + ), + ), + appBar: AppBar( + // actions: [ + // InkWell( + // onTap: () { + // DialogHandler(context: context).showPrivateBots(); + // }, + // child: CircleIconBtn( + // size: Responsive(context).isMobile() ? 32 : 46, + // iconPadding: const EdgeInsets.all(8), + // icon: Assets.icon.outline.crown, + // color: context.read().isDark() + // ? AppColors.black[900] + // : AppColors.secondryColor[50], + // iconColor: Theme.of(context).colorScheme.secondary, + // ), + // ), + // const SizedBox( + // width: 16, + // ), + // ], + leading: Builder(builder: (context) { + return IconButton( + icon: const Icon(Icons.menu), + onPressed: () { + Scaffold.of(context).openDrawer(); + }, + ); + }), + ), + body: Stack( + children: [ + SizedBox( + width: double.infinity, + height: MediaQuery.sizeOf(context).height, + child: Assets.image.chatBack.image(fit: BoxFit.cover), + ), + Responsive(context).maxWidthInDesktop( + maxWidth: 800, + child: (contxet, maxWidth) { + maxWidthDesktop = maxWidth; + return SingleChildScrollView( + controller: ReceiveMessageCubit.scrollController, + reverse: true, + physics: + context.watch().state is MessagesLoading + ? const NeverScrollableScrollPhysics() + : const BouncingScrollPhysics(), + child: Column( + children: [ + messages(), + aNewMessage(), + if (widget.chatArgs.bot.attachment != 3) + relatedQuestions(), + const SizedBox( + height: 120, + ), + ValueListenableBuilder( + valueListenable: selectedFile, + builder: (context, value, child) => SizedBox( + height: value != null ? 70 : 0, + ), + ) + ], + ), + ); + }), + ], + ), + bottomSheet: messageBar(), + ), + ); + } + + Widget messages() { + return BlocConsumer( + listener: (context, state) { + if (state is MessagesSuccess) { + if (state.isGetAll) { + if (chatId != null && + widget.chatArgs.bot.attachment != 3 && + (widget.chatArgs.bot.tool != null && + !widget.chatArgs.bot.tool!)) { + try { + if (refreshQuestions.value) { + context + .read() + .add(GetAllRelatedQuestions( + chatId: chatId!, + messageId: state.messages + .lastWhere( + (element) => element.role == 'human', + ) + .id!, + content: state.messages.last.query ?? '', + )); + } + } catch (e) { + if (kDebugMode) { + print("Error while get Related Questions is: $e"); + } + } + } + } + } + }, + builder: (context, state) { + if (state is MessagesFail) { + return Padding( + padding: + EdgeInsets.only(top: MediaQuery.sizeOf(context).height * 0.1), + child: EmptyStates.getEmptyState(status: EmptyStatesEnum.server), + ); + } + if (state is MessagesLoading) { + return const ChatScreenPlaceholder(); + } + if (state is MessagesSuccess) { + return ListView.builder( + shrinkWrap: true, + itemCount: state.messages.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + final message = state.messages[index]; + final GlobalKey containerKey = GlobalKey(); + final GlobalKey markdownKey = + GlobalKey(); + ValueNotifier directionName = ValueNotifier('RTL'); + return GestureDetector( + onLongPress: () { + _popUpMenu(message, containerKey); + }, + child: Container( + alignment: message.fromBot! + ? Alignment.centerLeft + : Alignment.centerRight, + padding: const EdgeInsets.all(16), + child: Directionality( + textDirection: message.fromBot! + ? TextDirection.ltr + : TextDirection.rtl, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + constraints: BoxConstraints( + minWidth: Responsive(context).isMobile() + ? maxWidthDesktop * 0.4 + : maxWidthDesktop * 0.2, + maxWidth: Responsive(context).isMobile() + ? maxWidthDesktop * 0.8 + : maxWidthDesktop * 0.6), + decoration: BoxDecoration( + color: message.error! + ? AppColors.red.defaultShade + : message.fromBot! + ? Theme.of(context) + .colorScheme + .surface + : AppColors.primaryColor.defaultShade, + borderRadius: BorderRadius.circular(16) + .copyWith( + topRight: message.fromBot! + ? const Radius.circular(10) + : const Radius.circular(0), + bottomLeft: message.fromBot! + ? const Radius.circular(0) + : const Radius.circular(10))), + padding: const EdgeInsets.all(8), + child: message.content != null + ? Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + if (message.file != null) + message.file!.name.isVideo() + ? GestureDetector( + onTap: () => DialogHandler( + context: context) + .showVideoHero( + url: message + .file!.path), + child: Container( + constraints: + const BoxConstraints( + maxWidth: double.infinity, + ), + child: VideoThumbnailWidget( + videoUrl: + message.file!.path), + ), + ) + : message.file!.name.isImage() + ? GestureDetector( + onTap: () => + DialogHandler( + context: + context) + .showImageHero( + image: message + .file! + .path), + child: AspectRatio( + aspectRatio: 1 / 1, + child: Container( + constraints: + const BoxConstraints( + maxWidth: + double.infinity, + ), + child: ClipRRect( + borderRadius: + BorderRadius + .circular( + 8), + child: + CustomeImage( + src: message + .file!.path, + fit: BoxFit + .cover, + )), + ), + ), + ) + : message.file!.name.isAudio() + ? Player( + fileUrl: message + .file!.path, + inMessages: true, + ) + : Container( + decoration: BoxDecoration( + color: context + .read< + ThemeModeCubit>() + .isDark() + ? AppColors + .black[ + 900] + : AppColors + .gray + .defaultShade, + borderRadius: + BorderRadius + .circular( + 10)), + padding: + const EdgeInsets + .all(8), + constraints: + const BoxConstraints( + minHeight: + 64), + child: Row( + children: [ + SizedBox( + child: message + .file! + .name + .isDocument() + ? const Icon( + CupertinoIcons + .doc) + : const SizedBox + .shrink(), + ), + Expanded( + child: + Padding( + padding: const EdgeInsets + .symmetric( + horizontal: + 12.0), + child: Text( + message.file! + .name, + textDirection: message + .file! + .name + .startsWithEnglish() + ? TextDirection + .ltr + : TextDirection + .rtl, + style: const TextStyle( + fontSize: + 16), + overflow: + TextOverflow + .ellipsis, + maxLines: 2, + ), + )), + ], + ), + ), + ...List.generate( + message.content!.length, + (index) { + final content = + message.content![index]; + return Column( + children: [ + if (content.audioUrl != null) + Player( + fileUrl: + content.audioUrl!.url ?? + '', + inMessages: true, + ), + if (content.imageUrl != null) + Container( + constraints: + const BoxConstraints( + maxWidth: double.infinity, + ), + child: AspectRatio( + aspectRatio: 1 / 1, + child: ImageNetwork( + url: content.imageUrl + ?.url ?? + '', + showHero: true, + radius: 10, + ), + ), + ), + if (content.pdfUrl != null) + Container( + decoration: BoxDecoration( + color: context + .read< + ThemeModeCubit>() + .isDark() + ? AppColors + .black[900] + : AppColors.gray + .defaultShade, + borderRadius: + BorderRadius + .circular(10)), + padding: + const EdgeInsets.all(8), + constraints: + const BoxConstraints( + minHeight: 64), + child: Row( + children: [ + const SizedBox( + child: Icon( + CupertinoIcons + .doc), + ), + Expanded( + child: Padding( + padding: + const EdgeInsets + .symmetric( + horizontal: + 12.0), + child: Text( + content.pdfUrl!.url + ?.split('/') + .last ?? + '', + textDirection: (content + .pdfUrl! + .url + ?.split( + '/') + .last ?? + '') + .startsWithEnglish() + ? TextDirection + .ltr + : TextDirection + .rtl, + style: + const TextStyle( + fontSize: + 16), + overflow: + TextOverflow + .ellipsis, + maxLines: 2, + ), + )), + ], + ), + ), + if (content.audioUrl == null && + content.imageUrl == null && + content.pdfUrl == null) + Padding( + padding: const EdgeInsets + .symmetric( + horizontal: 8.0), + child: Builder( + builder: (context) { + directionName + .value = content + .text != + null && + content.text! + .startsWithEnglish() + ? "LTR" + : 'RTL'; + return DefaultMarkdownText( + key: markdownKey, + text: content.text ?? + '', + fromBot: + message.fromBot!, + color: message + .fromBot! + ? Theme.of( + context) + .colorScheme + .onSurface + : Colors.white); + }), + ) + ], + ); + }, + ), + if (message.fromBot!) + Padding( + padding: const EdgeInsets.only( + top: 8.0, left: 8.0), + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + ValueListenableBuilder( + valueListenable: + directionName, + builder: (context, dir, _) { + return InkWell( + onTap: () { + directionName + .value = markdownKey + .currentState + ?.changeDirection() ?? + 'RTL'; + }, + child: Container( + padding: + const EdgeInsets + .symmetric( + horizontal: + 12, + vertical: 4), + decoration: BoxDecoration( + borderRadius: + BorderRadius + .circular( + 4), + color: context + .read< + ThemeModeCubit>() + .isDark() + ? AppColors + .black[ + 900] + : AppColors + .primaryColor[ + 50]), + child: Text( + dir, + style: TextStyle( + color: Theme.of( + context) + .colorScheme + .primary), + ), + ), + ); + }), + Row( + mainAxisSize: + MainAxisSize.min, + children: [ + BlocProvider< + LikeMessageCubit>( + create: (context) => + LikeMessageCubit() + ..getLike( + like: message + .like), + child: BlocBuilder< + LikeMessageCubit, + LikeMessageState>( + builder: + (context, state) { + return DefaultPlaceHolder( + enabled: state + is LikeMessageLoading, + child: Row( + children: [ + GestureDetector( + onTap: + () async { + await context.read().setLike( + like: state + is LikeMessageLiked + ? null + : true, + chatId: + chatId!, + messageId: + message.id!); + }, + child: + Padding( + padding: const EdgeInsets + .only( + right: + 20.0), + child: + SizedBox( + width: 16, + height: + 16, + child: state + is LikeMessageLiked + ? Assets + .icon + .bold + .like + .svg(color: AppColors.green.defaultShade) + : Assets.icon.outline.like.svg(color: Theme.of(context).colorScheme.primary), + ), + ), + ), + GestureDetector( + onTap: + () async { + await context.read().setLike( + like: state + is LikeMessageDisLiked + ? null + : false, + chatId: + chatId!, + messageId: + message.id!); + }, + child: + Padding( + padding: const EdgeInsets + .only( + right: + 20.0), + child: + SizedBox( + width: 16, + height: + 16, + child: state + is LikeMessageDisLiked + ? Assets + .icon + .bold + .dislike + .svg(color: AppColors.red.defaultShade) + : Assets.icon.outline.dislike.svg(color: Theme.of(context).colorScheme.primary), + ), + ), + ), + ], + ), + ); + }, + ), + ), + if (widget.chatArgs.bot + .tool != + null && + !widget + .chatArgs.bot.tool!) + MorePopupMenuHandler( + context: context) + .morePopUpMenu( + child: Padding( + padding: + const EdgeInsets + .only( + right: + 20.0), + child: + CircleIconBtn( + icon: Assets + .icon + .outline + .magicpen, + color: context + .read< + ThemeModeCubit>() + .isDark() + ? AppColors + .black[ + 900] + : AppColors + .primaryColor[50], + iconColor: Theme.of( + context) + .colorScheme + .primary, + size: 28, + iconPadding: + const EdgeInsets + .all( + 6), + ), + ), + items: [ + PopUpMenuItemModel( + click: () { + try { + refreshQuestions + .value = + false; + sendRequest( + file: message + .file, + message: + 'خلاصه‌تر بنویس'); + } catch (e) { + if (kDebugMode) { + print( + 'Error is: $e'); + } + } + }, + popupMenuItem: PopupMenuItem( + value: 0, + child: MorePopupMenuHandler.morePopUpItem( + icon: Assets + .icon + .outline + .eraser, + title: + 'خلاصه‌تر بنویس')), + ), + PopUpMenuItemModel( + click: () { + try { + refreshQuestions + .value = + false; + sendRequest( + file: message + .file, + message: + 'کامل بنویس'); + } catch (e) { + if (kDebugMode) { + print( + 'Error is: $e'); + } + } + }, + popupMenuItem: PopupMenuItem( + value: 1, + child: MorePopupMenuHandler.morePopUpItem( + icon: Assets + .icon + .outline + .edit2, + title: + 'کامل بنویس')), + ), + PopUpMenuItemModel( + popupMenuItem: PopupMenuItem( + value: 2, + child: MorePopupMenuHandler.morePopUpItem( + icon: Assets + .icon + .outline + .voiceCricle, + title: + 'لحن نوشته را تغییر بده')), + click: () async { + await BottomSheetHandler( + context) + .showStringList( + onSelect: + (value) { + try { + refreshQuestions.value = + false; + sendRequest( + file: message.file, + message: 'لحن نوشته را تغییر بده به $value'); + } catch (e) { + if (kDebugMode) { + print('Error is: $e'); + } + } + }, + title: 'انتخاب لحن نوشته', + values: [ + 'رسمی', + 'عامیانه', + 'دوستانه', + 'حرفه ای', + 'محاوره ای', + 'طنز', + 'جدی' + ]); + }, + ), + PopUpMenuItemModel( + popupMenuItem: PopupMenuItem( + value: 3, + child: MorePopupMenuHandler.morePopUpItem( + icon: Assets + .icon + .outline + .translate, + title: + 'ترجمه کن')), + click: () async { + await BottomSheetHandler( + context) + .showStringList( + onSelect: + (value) { + try { + refreshQuestions.value = + false; + sendRequest( + file: message.file, + message: 'زبان نوشته را تغییر بده به $value'); + } catch (e) { + if (kDebugMode) { + print('Error is: $e'); + } + } + }, + title: + 'انتخاب زبان', + values: [ + '🇮🇷 فارسی', + 'Arabic 🇸🇦', + 'Bengali 🇧🇩', + 'English 🇬🇧', + 'French 🇫🇷', + 'German 🇩🇪', + 'Hindi 🇮🇳', + 'Italian 🇮🇹' + ]); + }, + ), + ]), + ], + ), + ], + ), + ) + ], + ) + : null, + ), + const SizedBox( + width: 4, + ), + message.error! + ? CircleIconBtn( + key: containerKey, + icon: Assets.icon.outline.bitcoinRefresh, + iconColor: AppColors.red.defaultShade, + iconPadding: const EdgeInsets.all(6), + onTap: () async { + if (message.query != null) { + refreshQuestions.value = true; + sendRequest( + file: message.file, + withOutNewMessage: true, + message: message.query!, + ); + context.read().add( + ChangeMessage( + oldMessage: message, + newMessage: message.copyWith( + error: false))); + } + }, + ) + : Transform.rotate( + angle: pi / 2, + child: CircleIconBtn( + color: message.fromBot! + ? context + .read() + .isDark() + ? AppColors.black[900] + : AppColors.primaryColor[50] + : AppColors.primaryColor.defaultShade, + iconColor: message.fromBot! + ? Theme.of(context) + .colorScheme + .primary + : Colors.white, + key: containerKey, + icon: Assets.icon.outline.more, + iconPadding: const EdgeInsets.all(6), + onTap: () async { + _popUpMenu(message, containerKey); + }, + ), + ) + ], + ), + Padding( + padding: const EdgeInsets.all(4.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (message.fromBot!) + // Row( + // children: [ + // ImageNetwork( + // url: bot.image ?? '', + // width: 16, + // height: 16, + // radius: 360, + // ), + // const SizedBox( + // width: 4, + // ), + // Text(bot.name ?? '', + // textDirection: TextDirection.rtl, + // style: AppTextStyles.body5.copyWith( + // fontWeight: FontWeight.bold, + // color: Theme.of(context) + // .colorScheme + // .onSurface)), + // const SizedBox( + // width: 8, + // ), + // ], + // ), + if (message.createdAt != null) + Text( + DateTimeUtils.convertToSentTime( + message.createdAt!), + style: AppTextStyles.body5.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface)), + ], + ), + ) + ], + ), + ), + ), + ); + }, + ); + } + return chatScreen(); + }, + ); + } + + Widget aNewMessage() { + return BlocConsumer( + builder: (context, state) { + if (state is ReceiveMessageOnResponsing) { + return Container( + alignment: Alignment.centerLeft, + child: Container( + padding: const EdgeInsets.all(16), + child: Container( + constraints: BoxConstraints( + minWidth: Responsive(context).isMobile() + ? maxWidthDesktop * 0.4 + : maxWidthDesktop * 0.2, + maxWidth: Responsive(context).isMobile() + ? maxWidthDesktop * 0.8 + : maxWidthDesktop * 0.6), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(10) + .copyWith(bottomLeft: const Radius.circular(0))), + padding: const EdgeInsets.all(16), + child: DefaultMarkdownText( + text: '${state.text}...', + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ); + } + if (state is ReceiveMessageLoading) { + return Container( + alignment: Alignment.centerLeft, + child: Container( + padding: const EdgeInsets.all(16), + child: Container( + width: Responsive(context).isMobile() + ? maxWidthDesktop * 0.8 + : maxWidthDesktop * 0.6, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(10) + .copyWith(bottomLeft: const Radius.circular(0))), + padding: const EdgeInsets.all(16), + child: Center( + child: SpinKitThreeBounce( + color: Theme.of(context).colorScheme.primary, + size: 32, + )), + ), + ), + ); + } + return const SizedBox.shrink(); + }, + listener: (context, state) { + if (state is ReceiveMessageDone) { + context.read().add(AddMessage(message: state.message)); + if (state.model.chatId != null) { + if (chatId == null && !isGhost.value) { + context.read().add(AddChat( + chats: Chats( + bot: bot, + title: state.model.chatTitle, + createdAt: DateTime.now().toIso8601String(), + id: state.model.chatId))); + } + chatId = state.model.chatId; + try { + if (widget.chatArgs.bot.attachment != 3 && + (widget.chatArgs.bot.tool != null && + !widget.chatArgs.bot.tool! && + refreshQuestions.value)) { + context.read().add(GetAllRelatedQuestions( + chatId: chatId!, + messageId: state.message.id!, + content: state.message.query!)); + } + } catch (e) { + if (kDebugMode) { + print('Error is: $e'); + } + } + } + final humanMessage = + context.read().state.messages.firstWhere( + (element) => element.id == state.oldHumanMessageId, + ); + if (state.model.humanMessageId != null) { + context.read().add(ChangeMessage( + oldMessage: humanMessage, + newMessage: + humanMessage.copyWith(id: state.model.humanMessageId))); + } + context.read().changeCredit(CreditModel( + credit: state.model.credit, freeCredit: state.model.freeCredit)); + } else if (state is ReceiveMessageOnFail) { + SnackBarManager(context, id: 'ReceiveMessageOnFail').show( + status: SnackBarStatus.error, + message: state.detail, + ); + final humanMessage = + context.read().state.messages.firstWhere( + (element) => element.id == state.oldHumanMessageId, + ); + context.read().add(ChangeMessage( + oldMessage: humanMessage, + newMessage: humanMessage.copyWith(error: true))); + } + }, + ); + } + + Widget relatedQuestions() { + return BlocBuilder( + builder: (context, state) { + if (state is RelatedQuestionsSuccess && + state.relatedQuestionsModel.questions != null && + state.relatedQuestionsModel.questions!.isNotEmpty) { + return Directionality( + textDirection: TextDirection.rtl, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Column( + children: [ + Row( + children: [ + Assets.icon.outline.messageQuestion.svg( + color: Theme.of(context).colorScheme.primary), + const SizedBox( + width: 4, + ), + Text( + 'سوالات مرتبط:', + style: AppTextStyles.body3.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary), + ), + ], + ) + ], + ), + const SizedBox( + height: 12, + ), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: state.relatedQuestionsModel.questions!.length, + itemBuilder: (context, index) { + final question = + state.relatedQuestionsModel.questions![index]; + return GestureDetector( + onTap: () { + refreshQuestions.value = true; + sendRequest(message: question); + }, + child: Directionality( + textDirection: question.startsWithEnglish() + ? TextDirection.ltr + : TextDirection.rtl, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0), + child: Text( + question, + textDirection: question.startsWithEnglish() + ? TextDirection.ltr + : TextDirection.rtl, + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[context + .read() + .isDark() + ? 600 + : 900]), + ), + ), + if (index != + state.relatedQuestionsModel.questions! + .length - + 1) + Padding( + padding: + const EdgeInsets.symmetric(vertical: 4.0), + child: Divider( + color: AppColors.gray.defaultShade, + ), + ) + ], + ), + ), + ); + }, + ), + ], + ), + ), + ); + } + return const SizedBox(); + }, + ); + } + + Widget chatScreen() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + widget.chatArgs.isPerson + ? Column( + children: [ + const SizedBox( + height: 16, + ), + ImageNetwork( + url: widget.chatArgs.bot.image ?? '', + width: 120, + height: 120, + radius: 16, + ), + const SizedBox( + height: 16, + ), + Text( + widget.chatArgs.bot.name ?? '', + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + if (widget.chatArgs.bot.description != null) + Container( + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).colorScheme.surface), + child: Center( + child: Text( + widget.chatArgs.bot.description!, + textDirection: TextDirection.rtl, + textAlign: TextAlign.justify, + style: AppTextStyles.body5.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + ), + ), + ], + ) + : bot.tool ?? false + ? Column( + children: [ + const SizedBox( + height: 24, + ), + Center( + child: ImageNetwork( + url: bot.image ?? '', + width: 64, + height: 64, + radius: 360, + color: + bot.image != null && bot.image!.contains('/llm') + ? Theme.of(context).colorScheme.onSurface + : null, + ), + ), + const SizedBox( + height: 8, + ), + Text( + bot.name ?? '', + textDirection: TextDirection.rtl, + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + if (bot.description != null) + LayoutBuilder(builder: (context, constraints) { + return ValueListenableBuilder( + valueListenable: maxLines, + builder: (context, lines, _) { + final span = TextSpan( + text: bot.description, + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[context + .read() + .isDark() + ? 600 + : 900])); + final tp = TextPainter( + text: span, + textDirection: TextDirection.ltr); + tp.layout(maxWidth: constraints.maxWidth); + final numLines = + tp.computeLineMetrics().length; + return Padding( + padding: + const EdgeInsets.fromLTRB(4, 12, 4, 4), + child: Stack( + children: [ + Column( + children: [ + Text( + bot.description!, + textDirection: TextDirection.rtl, + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[context + .read< + ThemeModeCubit>() + .isDark() + ? 600 + : 900]), + maxLines: (lines), + textAlign: TextAlign.justify, + ), + if (lines == null && numLines >= 5) + Transform.rotate( + angle: -pi / 2, + child: CircleIconBtn( + onTap: () { + if (maxLines.value == + null) { + maxLines.value = 5; + return; + } + maxLines.value = null; + }, + size: 46, + color: Theme.of(context) + .colorScheme + .primary, + iconColor: Colors.white, + icon: Assets.icon.outline + .arrowRight)), + ], + ), + if (lines != null && numLines > lines) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + height: 64, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context) + .scaffoldBackgroundColor, + Theme.of(context) + .scaffoldBackgroundColor + .withAlpha(140) + ], + begin: + Alignment.bottomCenter, + end: Alignment.topCenter, + ), + ), + alignment: + Alignment.bottomCenter, + child: SizedBox( + child: Transform.rotate( + angle: pi / 2, + child: CircleIconBtn( + onTap: () { + if (maxLines + .value == + null) { + maxLines.value = + 5; + return; + } + maxLines.value = + null; + }, + size: 46, + color: + Theme.of(context) + .colorScheme + .primary, + iconColor: + Colors.white, + icon: Assets + .icon + .outline + .arrowRight)), + ), + )) + ], + ), + ); + }); + }), + const SizedBox( + height: 8, + ), + ], + ) + : Container( + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).colorScheme.surface), + child: Center( + child: Text( + 'سلام! به هوشان خوش اومدی. من اینجا هستم تا پاسخگوی سوالاتت باشم. امیدوارم در استفاده از هوشان تجربه خوبی داشته باشی!', + textDirection: TextDirection.rtl, + textAlign: TextAlign.justify, + style: AppTextStyles.body5.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ValueListenableBuilder( + valueListenable: isGhost, + builder: (context, g, _) { + return Transform.scale( + scale: 0.8, + child: Switch.adaptive( + value: g, + thumbIcon: WidgetStateProperty.resolveWith( + (Set states) { + if (states.contains(WidgetState.selected)) { + return Icon(CustomIcons.ghost, + color: Theme.of(context) + .colorScheme + .onSurface); + } + return Icon(Icons.close, + color: Theme.of(context) + .colorScheme + .onSurface); + }, + ), + onChanged: (value) { + isGhost.value = value; + }, + ), + ); + }), + const SizedBox( + width: 8, + ), + Text( + 'حالت ناشناس', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold), + ), + const SizedBox( + width: 8, + ), + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: HintTooltip( + hint: + 'با فعال کردن این گزینه؛ چت‌های شما در قسمت تاریخچه، ذخیره نمی‌شوند و اطلاعاتتان ناشناس باقی می‌ماند.', + iconColor: Theme.of(context).colorScheme.onSurface, + ), + ) + ], + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + borderRadius: BorderRadius.circular(12)), + child: Row( + children: [ + Text( + bot.cost == 0 || bot.cost == null + ? 'رایگان' + : bot.cost.toString(), + style: AppTextStyles.body3.copyWith(color: Colors.white), + ), + const SizedBox( + width: 4, + ), + Assets.icon.outline.coin + .svg(color: Colors.white, width: 18, height: 18) + ], + ), + ), + ], + ), + ], + ), + ); + } + + Widget messageBar() { + return Directionality( + textDirection: TextDirection.rtl, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ValueListenableBuilder( + valueListenable: selectedFile, + builder: (context, value, child) { + if (value != null && !showRecorder.value) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(10)), + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(8), + constraints: const BoxConstraints(minHeight: 64), + child: Row( + children: [ + SizedBox( + child: value.name.isImage() + ? GestureDetector( + onTap: () => DialogHandler(context: context) + .showImageHero(image: value.path), + child: SizedBox( + width: 46, + child: AspectRatio( + aspectRatio: 3 / 4, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: CustomeImage( + src: value.path, + fit: BoxFit.cover, + )), + ), + ), + ) + : value.path.isDocument() + ? const Icon(CupertinoIcons.doc) + : value.path.isAudio() + ? Player( + fileUrl: value.path, + inMessages: true, + ) + : const SizedBox.shrink(), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Text( + value.name, + textDirection: value.name.startsWithEnglish() + ? TextDirection.ltr + : TextDirection.rtl, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + )), + CircleIconBtn( + icon: Assets.icon.outline.trash, + color: AppColors.red[50], + iconColor: AppColors.red.defaultShade, + iconPadding: const EdgeInsets.all(6), + onTap: () { + selectedFile.value = null; + }, + ) + ], + ), + ); + } + return const SizedBox.shrink(); + }, + ), + Container( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + margin: Responsive(context).isMobile() + ? EdgeInsets.zero + : const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(32), topRight: Radius.circular(32)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + offset: const Offset(0, 0), + blurRadius: 12, + spreadRadius: 0, + ), + ], + ), + child: Column( + children: [ + ValueListenableBuilder( + valueListenable: showRecorder, + builder: (context, inRecord, child) { + return inRecord + ? Recorder( + play: true, + onDelete: () { + selectedFile.value = null; + showRecorder.value = false; + }, + onError: (e) { + showRecorder.value = false; + recording.value = false; + }, + onRecordFinish: (file) { + selectedFile.value = file; + recording.value = false; + }, + ) + : Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: widget.chatArgs.bot.attachment == 3 + ? const SizedBox.shrink() + : SizedBox( + child: ValueListenableBuilder( + valueListenable: selectedFile, + builder: (context, file, child) { + return ValueListenableBuilder( + valueListenable: messageText, + builder: + (context, text, child) { + return Directionality( + textDirection: text.text + .startsWithEnglish() + ? TextDirection.ltr + : TextDirection.rtl, + child: TextField( + controller: messageText, + onChanged: (value) {}, + + enabled: (bot.deleted != + null && + !bot + .deleted!) && + (context + .watch< + ReceiveMessageCubit>() + .state + is! ReceiveMessageOnResponsing && + context + .watch< + ReceiveMessageCubit>() + .state + is! ReceiveMessageLoading) && + !(bot.attachment == + 1 && + file != null) && + (widget.chatArgs.bot + .attachment != + 3), + minLines: 1, + maxLines: 6, // Set this + keyboardType: + TextInputType + .multiline, + style: AppTextStyles + .body4 + .copyWith( + color: Theme.of( + context) + .colorScheme + .onSurface), + decoration: + InputDecoration( + contentPadding: + const EdgeInsets + .fromLTRB( + 0, 12, 0, 12), + filled: true, + hintText: (bot.deleted != + null && + bot.deleted!) + ? 'دستیار مورد نظر توسط سازنده حذف شده است!' + : 'چیزی بنویسید ...', + hintStyle: + AppTextStyles + .body4, + fillColor: Colors + .transparent, + border: + const OutlineInputBorder( + borderSide: + BorderSide.none, + ), + ), + ), + ); + }); + }, + ), + ), + ), + const SizedBox( + width: 16, + ), + ], + ); + }), + Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + if ((bot.deleted != null && !bot.deleted!)) + ValueListenableBuilder( + valueListenable: selectedFile, + builder: (context, file, child) { + return ValueListenableBuilder( + valueListenable: recording, + builder: (context, inRecording, child) { + return ValueListenableBuilder( + valueListenable: showRecorder, + builder: + (context, showRecord, child) { + return ValueListenableBuilder( + valueListenable: messageText, + builder: (context, message, + child) { + return inRecording + ? const SizedBox + .shrink() + : bot.attachmentType != + null && + bot.attachmentType! + .contains( + 'audio') && + message.text + .replaceAll( + ' ', '') + .isEmpty && + !showRecord && + file == null + ? GestureDetector( + onTap: () { + showRecorder + .value = + true; + recording + .value = + true; + }, + child: Assets + .icon + .outline + .microphoneChat + .svg( + width: + 24, + height: + 24, + color: Theme.of(context) + .colorScheme + .primary)) + : GestureDetector( + onTap: () { + if ((messageText + .text + .replaceAll(' ', + '') + .isEmpty && + widget.chatArgs.bot.attachment != + 3) && + selectedFile + .value == + null) { + return; + } + if (selectedFile + .value != + null && + bot.attachment == + 1) { + return; + } + refreshQuestions + .value = + true; + sendRequest( + file: selectedFile + .value, + message: + messageText + .text); + }, + child: Assets + .icon + .bold + .send + .svg( + width: + 24, + height: 24)); + }, + ); + }); + }); + }), + const SizedBox( + width: 8, + ), + // CircleIconBtn( + // icon: Assets.icon.outline.infoCircle, + // color: Theme.of(context).colorScheme.surface, + // iconColor: Theme.of(context).colorScheme.onSurface, + // onTap: () { + // showInfo.value = !showInfo.value; + // }, + // ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ValueListenableBuilder( + valueListenable: webSearch, + builder: (context, isWebSearchEnabled, _) { + return GestureDetector( + onTap: () { + webSearch.value = !webSearch.value; + }, + child: HintTooltip( + hint: 'جستجو در وب', + child: Assets.icon.outline.globalSearch.svg( + width: 24, + height: 24, + color: isWebSearchEnabled + ? Theme.of(context) + .colorScheme + .primary + : Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.4), + ), + ), + ); + }, + ), + // ValueListenableBuilder( + // valueListenable: webSearch, + // builder: (context, canWebSearch, _) { + // return ChoiceChip( + // padding: EdgeInsets.zero, + // showCheckmark: false, + // labelPadding: + // const EdgeInsets.symmetric(horizontal: 8), + // selectedColor: Theme.of(context) + // .colorScheme + // .primary, // Change selected color + // backgroundColor: + // Theme.of(context).colorScheme.surface, + // surfaceTintColor: Colors.transparent, + // selectedShadowColor: Colors.transparent, + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(360)), + // onSelected: (value) { + // webSearch.value = value; + // }, + // label: Row( + // children: [ + // Assets.icon.outline.globalSearch.svg( + // color: canWebSearch + // ? Colors.white + // : Theme.of(context) + // .colorScheme + // .onSurface), + // const SizedBox( + // width: 4, + // ), + // Text( + // 'جستجو در وب', + // style: AppTextStyles.body5.copyWith( + // color: canWebSearch + // ? Colors.white + // : Theme.of(context) + // .colorScheme + // .onSurface), + // ), + // ], + // ), + // selected: canWebSearch); + // }), + const SizedBox( + width: 12, + ), + if ((bot.deleted != null && !bot.deleted!)) + if (bot.attachmentType != null && + bot.attachmentType!.isNotEmpty) + ValueListenableBuilder( + valueListenable: visibleAttach, + builder: (context, value, child) { + return AnimatedVisibility( + isVisible: value, + duration: + const Duration(milliseconds: 300), + fadeMode: FadeMode.horizontal, + child: Padding( + padding: + const EdgeInsets.only(left: 12.0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + if (bot.attachmentType! + .contains('image')) + Padding( + padding: + const EdgeInsets.only( + right: 8.0), + child: CircleIconBtn( + icon: Assets.icon.outline + .galleryAdd, + color: Theme.of(context) + .colorScheme + .primary, + iconColor: Colors.white, + onTap: () async { + await BottomSheetHandler( + context) + .showPickImage( + onSelect: (file) { + selectedFile.value = + file; + if (widget + .chatArgs + .bot + .attachment != + 3) { + visibleAttach + .value = + false; + } + }, + ); + }), + ), + if (bot.attachmentType! + .contains('audio')) + Padding( + padding: + const EdgeInsets.only( + right: 8.0), + child: CircleIconBtn( + icon: Assets + .icon.outline.musicnote, + color: Theme.of(context) + .colorScheme + .primary, + iconColor: Colors.white, + onTap: () async { + final file = + await PickFileService( + context) + .getFile( + fileType: + FileType + .audio); + if (file != null) { + selectedFile.value = + file.single; + if (widget.chatArgs.bot + .attachment != + 3) { + visibleAttach.value = + false; + } + } + }, + ), + ), + if (bot.attachmentType! + .contains('pdf')) + Padding( + padding: + const EdgeInsets.only( + right: 8.0), + child: CircleIconBtn( + icon: Assets + .icon.outline.cardAdd, + color: Theme.of(context) + .colorScheme + .primary, + iconColor: Colors.white, + onTap: () async { + final file = + await PickFileService( + context) + .getFile( + fileType: + FileType + .custom, + allowedExtensions: [ + 'pdf', + 'doc', + 'docx', + 'xls', + 'xlsx', + 'xlsm', + 'xlsb', + 'xlt', + 'xltx', + 'xltm' + ]); + if (file != null) { + selectedFile.value = + file.single; + if (widget + .chatArgs + .bot + .attachment != + 3) { + visibleAttach + .value = false; + } + } + }), + ) + ], + ), + )); + }, + ), + if ((bot.deleted != null && !bot.deleted!)) + if (bot.attachment != 0 && + bot.attachmentType != null && + bot.attachmentType!.isNotEmpty && + bot.attachment != 3) + GestureDetector( + onTap: () { + if (widget.chatArgs.bot.attachment != 3) { + visibleAttach.value = + !visibleAttach.value; + } + }, + child: Assets.icon.outline.elementPlus.svg( + width: 24, + height: 24, + color: Theme.of(context) + .colorScheme + .primary)), + ], + ), + ], + ), + SizedBox( + height: 10, + ), + ], + ), + const SizedBox( + height: 4, + ), + ValueListenableBuilder( + valueListenable: showInfo, + builder: (context, show, _) { + if (show) { + return Text( + 'مدل‌های هوش مصنوعی می‌توانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید و از وارد کردن اطلاعات حساس بپرهیزید.', + style: AppTextStyles.body6.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ); + } + return const SizedBox.shrink(); + }) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/screens/chat/cubit/like_message_cubit.dart b/lib/ui/screens/chat/cubit/like_message_cubit.dart new file mode 100644 index 0000000..b2aa9ec --- /dev/null +++ b/lib/ui/screens/chat/cubit/like_message_cubit.dart @@ -0,0 +1,40 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/data/repository/chatbot_repository.dart'; + +part 'like_message_state.dart'; + +class LikeMessageCubit extends Cubit { + LikeMessageCubit() : super(LikeMessageInitial()); + + void getLike({ + required bool? like, + }) { + if (like == null) { + emit(LikeMessageUnLiked()); + } else { + emit(like ? LikeMessageLiked() : LikeMessageDisLiked()); + } + } + + Future setLike( + {required bool? like, + required final int chatId, + required final String messageId}) async { + emit(LikeMessageLoading()); + try { + await ChatbotRepository.likedMessage( + chatId: chatId, messageId: messageId, like: like); + if (like == null) { + emit(LikeMessageUnLiked()); + } else { + emit(like ? LikeMessageLiked() : LikeMessageDisLiked()); + } + } catch (e) { + if (kDebugMode) { + print("Dio Error: $e"); + } + } + } +} diff --git a/lib/ui/screens/chat/cubit/like_message_state.dart b/lib/ui/screens/chat/cubit/like_message_state.dart new file mode 100644 index 0000000..56b90b0 --- /dev/null +++ b/lib/ui/screens/chat/cubit/like_message_state.dart @@ -0,0 +1,20 @@ +part of 'like_message_cubit.dart'; + +sealed class LikeMessageState extends Equatable { + const LikeMessageState(); + + @override + List get props => []; +} + +final class LikeMessageInitial extends LikeMessageState {} + +final class LikeMessageLiked extends LikeMessageState {} + +final class LikeMessageDisLiked extends LikeMessageState {} + +final class LikeMessageUnLiked extends LikeMessageState {} + +final class LikeMessageLoading extends LikeMessageState {} + +final class LikeMessageFail extends LikeMessageState {} diff --git a/lib/ui/screens/chat/cubit/receive_message_cubit.dart b/lib/ui/screens/chat/cubit/receive_message_cubit.dart new file mode 100644 index 0000000..69906bb --- /dev/null +++ b/lib/ui/screens/chat/cubit/receive_message_cubit.dart @@ -0,0 +1,203 @@ +import 'dart:convert'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:hoshan/core/utils/strings.dart'; +import 'package:hoshan/data/model/ai/ai_response_model.dart'; +import 'package:hoshan/data/model/ai/messages_model.dart'; +import 'package:hoshan/data/model/ai/send_message_model.dart'; +import 'package:hoshan/data/repository/chatbot_repository.dart'; +import 'package:string_validator/string_validator.dart'; + +part 'receive_message_state.dart'; + +class ReceiveMessageCubit extends Cubit { + ReceiveMessageCubit() : super(ReceiveMessageInitial()); + + static ScrollController scrollController = ScrollController(); + static ValueNotifier onResponse = ValueNotifier(false); + + static Future scrollToEnd({final double? extra}) async { + try { + await scrollController.animateTo( + scrollController.position.minScrollExtent + (extra ?? 0), + duration: const Duration(milliseconds: 600), + curve: Curves.easeInOut, + ); + } catch (e) { + if (kDebugMode) { + // print('Error in Scroll:$e'); + } + } + } + + static Future scrollToStart({final double? extra}) async { + try { + await scrollController.animateTo( + scrollController.position.maxScrollExtent + (extra ?? 0), + duration: const Duration(milliseconds: 600), + curve: Curves.easeInOut, + ); + } catch (e) { + if (kDebugMode) { + // print('Error in Scroll:$e'); + } + } + } + + void execute({required final SendMessageModel request}) async { + emit(ReceiveMessageLoading()); + onResponse.value = true; + String result = ''; + AiResponseModel aiResponseModel = AiResponseModel(); + await scrollToEnd(); + try { + // Call your streaming message function and yield states accordingly + await for (String message in request.tool! + ? ChatbotRepository.sendMessageTool(request) + : ChatbotRepository.sendMessage(request)) { + Map jsonMap; + try { + jsonMap = jsonDecode(message); + } catch (e) { + if (kDebugMode) { + print('Error in Parse: $e'); + } + jsonMap = {}; + jsonMap['content'] = ''; + try { + message = message.trim(); + + List jsonStrings = message.split('}{').map((s) { + if (!s.startsWith('{')) s = '{$s'; + if (!s.endsWith('}')) s = '$s}'; + return s; + }).toList(); + + for (String json in jsonStrings) { + json = json.replaceAll( + RegExp(r'[{}"]'), ''); // Remove braces and quotes + List pairs = json.split(', '); + for (String pair in pairs) { + if (pair.contains('chat_id:')) { + jsonMap['chat_id'] = + int.tryParse(pair.replaceAll('chat_id: ', '')); + } else if (pair.contains('chat_title:')) { + jsonMap['chat_title'] = pair.replaceAll('chat_title: ', ''); + } else if (pair.contains('ai_message_id:')) { + jsonMap['ai_message_id'] = + pair.replaceAll('ai_message_id: ', ''); + } else if (pair.contains('human_message_id:')) { + jsonMap['human_message_id'] = + pair.replaceAll('human_message_id: ', ''); + } else if (pair.contains('credit:')) { + jsonMap['credit'] = pair.replaceAll('credit: ', ''); + } else if (pair.contains('credit:')) { + jsonMap['credit'] = pair.replaceAll('credit: ', ''); + } else if (pair.contains('free:')) { + jsonMap['free'] = pair.replaceAll('free: ', ''); + } else if (pair.contains('human_message_created_at:')) { + jsonMap['human_message_created_at'] = + pair.replaceAll('human_message_created_at: ', ''); + } else if (pair.contains('ai_message_created_at:')) { + jsonMap['ai_message_created_at'] = + pair.replaceAll('ai_message_created_at: ', ''); + } else { + jsonMap['content'] += pair + .replaceAll('content: ', '') + .replaceAll('\\n', '\n\n'); + } + } + } + } catch (e) { + if (kDebugMode) { + print('Error in Manul Parse: $e'); + } + jsonMap = {}; + } + } + + final res = AiResponseModel.fromJson(jsonMap); + + if (res.content != null) { + result += res.content ?? ''; // Add each content to the list + if (!(res.content!.startsWith('http'))) { + emit(ReceiveMessageOnResponsing(text: result)); + } + } + aiResponseModel = aiResponseModel.copyWith( + error: res.error, + credit: res.credit, + detail: res.detail, + statusCode: res.statusCode, + aiMessageId: res.aiMessageId, + chatId: res.chatId, + chatTitle: res.chatTitle, + content: result.isEmpty ? res.content : result, + freeCredit: res.freeCredit, + humanMessageId: res.humanMessageId); + // Yield the received message line by line + } + await scrollToEnd(); + + if (aiResponseModel.error ?? true) { + emit(ReceiveMessageOnFail( + oldHumanMessageId: request.messageId ?? '', + detail: 'خطا از سمت سرور لطفا لحظاتی دیگر دوباره تلاش کنید', + statusCode: aiResponseModel.statusCode ?? 500)); + } else { + final message = Messages( + id: aiResponseModel.aiMessageId, + query: request.query, + role: 'ai', + content: [ + if (aiResponseModel.content != null) + Content( + audioUrl: aiResponseModel.content!.isURL() && + aiResponseModel.content!.isAudio() + ? FileUrl(url: aiResponseModel.content) + : null, + pdfUrl: aiResponseModel.content!.isURL() && + aiResponseModel.content!.isDocument() + ? FileUrl(url: aiResponseModel.content) + : null, + type: aiResponseModel.content!.isURL() + ? aiResponseModel.content!.isAudio() + ? 'audio' + : aiResponseModel.content!.isImage() + ? 'image' + : aiResponseModel.content!.isDocument() + ? 'doc' + : 'text' + : 'text', + text: aiResponseModel.content!.isURL() + ? null + : aiResponseModel.content, + imageUrl: aiResponseModel.content!.isURL() && + aiResponseModel.content!.isImage() + ? FileUrl(url: aiResponseModel.content) + : null) + ]); + emit(ReceiveMessageDone( + model: aiResponseModel, + message: message, + oldHumanMessageId: request.messageId!)); + } + } catch (e) { + // if (e.response?.statusCode == 403) { + // emit(const SendMessageError('موجودی شماکافی نمیباشد')); + // } else { + // emit(SendMessageError('Error: $e')); + // } + emit(ReceiveMessageOnFail( + detail: 'خطا از سمت سرور لطفا لحظاتی دیگر دوباره تلاش کنید', + statusCode: 500, + oldHumanMessageId: request.messageId ?? '')); + } + onResponse.value = false; + await Future.delayed(const Duration(milliseconds: 300)); + await scrollToEnd(); + } +} diff --git a/lib/ui/screens/chat/cubit/receive_message_state.dart b/lib/ui/screens/chat/cubit/receive_message_state.dart new file mode 100644 index 0000000..a316a90 --- /dev/null +++ b/lib/ui/screens/chat/cubit/receive_message_state.dart @@ -0,0 +1,43 @@ +part of 'receive_message_cubit.dart'; + +sealed class ReceiveMessageState extends Equatable { + const ReceiveMessageState(); + + @override + List get props => []; +} + +final class ReceiveMessageInitial extends ReceiveMessageState {} + +final class ReceiveMessageLoading extends ReceiveMessageState {} + +final class ReceiveMessageOnResponsing extends ReceiveMessageState { + final String text; + + const ReceiveMessageOnResponsing({required this.text}); + + @override + List get props => [text]; +} + +final class ReceiveMessageOnFail extends ReceiveMessageState { + final String detail; + final int statusCode; + final String oldHumanMessageId; + + const ReceiveMessageOnFail( + {required this.detail, + required this.statusCode, + required this.oldHumanMessageId}); +} + +final class ReceiveMessageDone extends ReceiveMessageState { + final AiResponseModel model; + final Messages message; + final String oldHumanMessageId; + + const ReceiveMessageDone( + {required this.model, + required this.message, + required this.oldHumanMessageId}); +} diff --git a/lib/ui/screens/cmp/cmp_page.dart b/lib/ui/screens/cmp/cmp_page.dart new file mode 100644 index 0000000..7bc9c11 --- /dev/null +++ b/lib/ui/screens/cmp/cmp_page.dart @@ -0,0 +1,352 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/ui/screens/cmp/cubit/cmp_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/dialog/dialog_handler.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:hoshan/ui/widgets/sections/header/reversible_appbar.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; +import 'package:persian_number_utility/persian_number_utility.dart'; + +class CmpPage extends StatefulWidget { + const CmpPage({super.key}); + + @override + State createState() => _CmpPageState(); +} + +class _CmpPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: ReversibleAppbar( + context, + titleText: 'مسابقات', + ), + body: Directionality( + textDirection: TextDirection.rtl, + child: BlocBuilder( + builder: (context, state) { + if (state is CmpFail) { + return const SizedBox(); + } + if (state is CmpSuccess) { + return ListView.builder( + itemCount: state.event.events?.length ?? 0, + physics: const BouncingScrollPhysics(), + shrinkWrap: true, + padding: const EdgeInsets.all(16), + itemBuilder: (context, index) { + final event = state.event.events![index]; + final enable = (event.isOpen ?? false); + return Column( + children: [ + Opacity( + opacity: enable ? 1 : 0.4, + child: ListTile( + leading: AspectRatio( + aspectRatio: 1 / 1, + child: Container( + width: 72, + height: 72, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Theme.of(context).colorScheme.surface), + padding: const EdgeInsets.all(8), + child: ImageNetwork( + url: event.image ?? '', + ), + ), + ), + title: Text( + event.title ?? '', + style: AppTextStyles.body3.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + subtitle: Text( + event.subtitle ?? '', + style: AppTextStyles.body5.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + ), + ), + event.isOpen ?? false + ? Column( + children: [ + ListTile( + leading: Assets.icon.outline.clock.svg( + color: + Theme.of(context).colorScheme.primary, + width: 24, + height: 24), + title: Text( + 'مهلت ارسال آثار: ${event.endAt?.toPersianDate()}', + style: AppTextStyles.body5.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + minTileHeight: 10, + ), + ], + ) + : Column( + children: [ + Opacity( + opacity: 0.4, + child: ListTile( + leading: Assets.icon.outline.clock.svg( + color: Theme.of(context) + .colorScheme + .primary, + width: 24, + height: 24), + title: Text( + 'تاریخ برگزاری: ${event.startAt?.toPersianDate()} لغایت ${event.endAt?.toPersianDate()}', + style: AppTextStyles.body5.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + minTileHeight: 10, + ), + ), + ListTile( + leading: Assets.icon.outline.galleryAdd.svg( + color: + Theme.of(context).colorScheme.primary, + width: 24, + height: 24), + title: Text( + 'تعداد کل آثار دریافتی: ${event.totalReceivedWorks} اثر', + style: AppTextStyles.body5.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + minTileHeight: 10, + ), + ListTile( + leading: Assets.icon.outline.profile.svg( + color: + Theme.of(context).colorScheme.primary, + width: 24, + height: 24), + title: Text( + 'تعداد کل شرکت‌کنندگان: ${event.totalParticipants} نفر', + style: AppTextStyles.body5.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + minTileHeight: 10, + ), + ], + ), + ListTile( + onTap: () { + DialogHandler(context: context) + .rewardForCmp(rewards: event.awards ?? ''); + }, + minTileHeight: 10, + leading: Assets.icon.outline.gift + .svg(color: Theme.of(context).colorScheme.primary), + title: Text( + 'جوایز مسابقه', + style: AppTextStyles.body5.copyWith( + color: Theme.of(context).colorScheme.primary), + ), + ), + const SizedBox( + height: 8, + ), + if (!(event.isOpen ?? true) && + event.winners != null && + event.winners!.isNotEmpty) + ListTile( + onTap: () { + DialogHandler(context: context) + .winnersForCmp(winners: event.winners!); + }, + minTileHeight: 10, + leading: Assets.icon.outline.profileTick.svg( + color: Theme.of(context).colorScheme.primary), + title: Text( + 'نفرات برتر مسابقه', + style: AppTextStyles.body5.copyWith( + color: Theme.of(context).colorScheme.primary), + ), + ), + const SizedBox( + height: 8, + ), + Opacity( + opacity: enable ? 1 : 0.4, + child: LoadingButton( + color: enable + ? AppColors.green.defaultShade + : AppColors.gray.defaultShade, + backgroundColor: enable?AppColors.green.defaultShade + : AppColors.green.defaultShade, + onPressed: enable + ? () { + if (event.description != null) { + DialogHandler(context: context) + .conditionsForCmp( + awards: event.description!); + } + } + : null, + width: double.infinity, + child: Text( + enable ? 'شرایط شرکت در مسابقه' : 'پایان یافته', + style: AppTextStyles.body4 + .copyWith(color:enable? Colors.white:Colors.black), + )), + ), + const SizedBox( + height: 16, + ), + if (index != state.event.events!.length - 1) + const Divider(), + const SizedBox( + height: 16, + ), + ], + ); + }, + ); + } + + return ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemCount: 10, + shrinkWrap: true, + padding: const EdgeInsets.all(16), + itemBuilder: (context, index) { + return Column( + children: [ + Row( + children: [ + SizedBox( + width: 72, + height: 72, + child: AspectRatio( + aspectRatio: 1 / 1, + child: DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: + Theme.of(context).colorScheme.surface), + padding: const EdgeInsets.all(8), + child: const SizedBox(), + ), + ), + ), + ), + const SizedBox( + width: 16, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8)), + child: Text( + 'event.title ?? ' '', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .primary), + ), + ), + ), + const SizedBox( + height: 8, + ), + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8)), + child: Text( + ' event.description ?? ' '', + style: AppTextStyles.body6.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + ), + ) + ], + ) + ], + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + DefaultPlaceHolder( + child: Container( + width: 300, + height: 32, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8)), + ), + ), + ], + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + DefaultPlaceHolder( + child: Container( + width: 250, + height: 32, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8)), + ), + ), + ], + ), + const SizedBox( + height: 8, + ), + LoadingButton( + loading: true, + width: double.infinity, + color: AppColors.gray[800], + child: Text( + 'شرایط شرکت در مسابقه', + style: + AppTextStyles.body4.copyWith(color: Colors.white), + )), + const SizedBox( + height: 16, + ), + if (index != 2) const Divider(), + const SizedBox( + height: 16, + ), + ], + ); + }, + ); + }, + ), + ), + ); + } +} diff --git a/lib/ui/screens/cmp/cubit/cmp_cubit.dart b/lib/ui/screens/cmp/cubit/cmp_cubit.dart new file mode 100644 index 0000000..b2ad43f --- /dev/null +++ b/lib/ui/screens/cmp/cubit/cmp_cubit.dart @@ -0,0 +1,25 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/event_model.dart'; +import 'package:hoshan/data/repository/bot_repository.dart'; + +part 'cmp_state.dart'; + +class CmpCubit extends Cubit { + CmpCubit() : super(CmpInitial()); + + void getAllEvents() async { + emit(CmpLoading()); + try { + final response = await BotRepository.getAllEvents(); + emit(CmpSuccess(event: response)); + } on DioException catch (e) { + emit(CmpFail()); + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } +} diff --git a/lib/ui/screens/cmp/cubit/cmp_state.dart b/lib/ui/screens/cmp/cubit/cmp_state.dart new file mode 100644 index 0000000..7f7fe6b --- /dev/null +++ b/lib/ui/screens/cmp/cubit/cmp_state.dart @@ -0,0 +1,20 @@ +part of 'cmp_cubit.dart'; + +sealed class CmpState extends Equatable { + const CmpState(); + + @override + List get props => []; +} + +final class CmpInitial extends CmpState {} + +final class CmpLoading extends CmpState {} + +final class CmpFail extends CmpState {} + +final class CmpSuccess extends CmpState { + final EventModel event; + + const CmpSuccess({required this.event}); +} diff --git a/lib/ui/screens/family/add_family.dart b/lib/ui/screens/family/add_family.dart new file mode 100644 index 0000000..2f8f7ad --- /dev/null +++ b/lib/ui/screens/family/add_family.dart @@ -0,0 +1,1261 @@ +// ignore_for_file: avoid_print, use_build_context_synchronously, unused_local_variable, deprecated_member_use + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/data/model/empty_states_enum.dart'; +import 'package:hoshan/data/repository/auth_repository.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/circle_icon_btn.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; +import 'package:hoshan/ui/widgets/sections/empty/empty_states.dart'; +import 'package:hoshan/ui/widgets/sections/header/primary_appbar.dart'; + +class AddFamily extends StatefulWidget { + const AddFamily({super.key}); + + @override + State createState() => _AddFamilyState(); +} + +class _AddFamilyState extends State { + @override + void initState() { + super.initState(); + _fetchFamilyMembers(); + } + + bool _showInviteForm = false; + + int? _selectedAgeIndex; + + final TextEditingController _phoneController = TextEditingController(); + + final List> _invitedMembers = []; + + static const int _maxMembers = 5; + + @override + void dispose() { + _phoneController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final isDark = theme.brightness == Brightness.dark; + final backgroundColor = colorScheme.background; + final surfaceColor = colorScheme.surface; + final onSurface = colorScheme.onSurface; + final borderColor = const Color.fromARGB(255, 207, 206, 205); + final accent = colorScheme.primary; + final success = colorScheme.secondary; + + final int invitedMembersCount = _invitedMembers.length; + + return Scaffold( + appBar: PrimaryAppbar( + context, + onBack: () => context.pop(), + actions: [ + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: GestureDetector( + onTap: () => context.go(Routes.myAccount), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + CircleIconBtn( + size: Responsive(context).isMobile() ? 32 : 46, + iconPadding: Responsive(context).isMobile() + ? null + : const EdgeInsets.all(8), + icon: Assets.icon.outline.coin, + color: context.read().isDark() + ? AppColors.black[900] + : AppColors.secondryColor[50], + iconColor: Theme.of(context).colorScheme.secondary, + ) + .animate( + autoPlay: true, + onPlay: (controller) => + controller.repeat(reverse: true), + ) + .moveY( + begin: 0, + end: -15, + duration: 800.ms, + curve: Curves.easeInBack, + delay: 30.seconds, + ), + ], + ), + ), + ), + ), + ], + ), + body: Directionality( + textDirection: TextDirection.rtl, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildTopInfoCard( + context, + title: 'امکانات نامحدود', + icon: 'assets/icon/outline/ion_infinite.svg', + ), + const SizedBox(width: 8), + _buildTopInfoCard( + context, + title: '5 عضو خانواده', + icon: 'assets/icon/outline/carbon_pedestrian-family.svg', + ), + const SizedBox(width: 8), + _buildTopInfoCard( + context, + title: 'سکه باسا', + icon: 'assets/icon/outline/streamline_coins-stack.svg', + ), + ], + ), + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isDark + ? surfaceColor + : const Color.fromARGB(255, 233, 232, 231), + borderRadius: BorderRadius.circular(16), + ), + child: Text( + 'در صورت اتمام شارژ، می‌توانید با پرداخت هزینه مجددا سکه دریافت کنید یا تا تمدید بعدی اعتبار باساکارت منتظر بمانید.', + style: AppTextStyles.body5.copyWith(color: onSurface), + textAlign: TextAlign.justify, + ), + ), + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: surfaceColor, + border: Border.all(color: borderColor), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'مدیریت خانواده', + style: AppTextStyles.body3.copyWith( + color: accent, + fontWeight: FontWeight.bold, + ), + ), + RichText( + text: TextSpan( + style: AppTextStyles.body3 + .copyWith(fontFamily: 'Dana'), + children: [ + TextSpan( + text: '$invitedMembersCount', + style: TextStyle( + color: accent, + fontWeight: FontWeight.bold, + fontSize: 25, + ), + ), + TextSpan( + text: '/$_maxMembers', + style: TextStyle( + color: + Color.fromARGB(255, 173, 173, 173), + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'دعوت و مدیریت اعضای خانواده', + style: AppTextStyles.body6.copyWith( + color: Color.fromARGB(255, 173, 173, 173), + fontWeight: FontWeight.bold), + ), + Text( + 'اعضای خانواده', + style: AppTextStyles.body6.copyWith( + color: Color.fromARGB(255, 173, 173, 173), + fontWeight: FontWeight.bold), + ), + ], + ), + ], + ), + const SizedBox(height: 20), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Directionality( + textDirection: TextDirection.ltr, + child: TweenAnimationBuilder( + duration: const Duration(milliseconds: 1250), + curve: Curves.easeOutQuart, + tween: Tween( + begin: 0, + end: invitedMembersCount / _maxMembers, + ), + builder: (context, value, _) { + return LinearProgressIndicator( + value: value, + minHeight: 6, + backgroundColor: onSurface.withOpacity(0.1), + valueColor: AlwaysStoppedAnimation( + Color.fromARGB(255, 34, 197, 94)), + ); + }, + ), + ), + ), + const SizedBox(height: 20), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Align( + alignment: Alignment.centerRight, + child: Text( + '${_maxMembers - invitedMembersCount} جایگاه خالی', + style: AppTextStyles.body5.copyWith( + color: accent, + fontWeight: FontWeight.normal, + ), + ), + ), + if (invitedMembersCount < _maxMembers) ...[ + const SizedBox(height: 12), + ElevatedButton( + onPressed: () { + setState(() { + _showInviteForm = !_showInviteForm; + }); + }, + style: ElevatedButton.styleFrom( + backgroundColor: isDark + ? success.withOpacity(0.15) + : const Color.fromARGB(255, 187, 247, 208), + foregroundColor: onSurface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + elevation: 0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + _showInviteForm + ? CupertinoIcons.minus + : CupertinoIcons.add, + size: 18, + ), + const SizedBox(width: 4), + Text( + _showInviteForm + ? 'بستن فرم' + : 'افزودن عضو جدید', + style: AppTextStyles.body5.copyWith( + color: onSurface, + fontWeight: FontWeight.bold), + ), + ], + ), + ), + ], + AnimatedSize( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: _showInviteForm + ? Padding( + padding: const EdgeInsets.only(top: 24), + child: _buildInviteCard( + invitedMembersCount + 1), + ) + : const SizedBox.shrink(), + ), + ], + ), + ], + ), + ), + if (_invitedMembers.isEmpty) + EmptyStates.getEmptyState( + status: EmptyStatesEnum.familyMembers, + title: 'هنوز عضوی اضافه نشده است.', + ) + else + _buildInvitedMembersList(), + ], + ), + ), + ), + ), + ); + } + + Widget _buildInvitedMembersList() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 12, top: 20), + child: Text( + 'افراد دعوت شده', + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + SizedBox(height: 10), + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _invitedMembers.length, + separatorBuilder: (context, index) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final member = _invitedMembers[index]; + return _buildInvitedMemberCard(member, index); + }, + ), + ], + ); + } + + Widget _buildInvitedMemberCard(Map member, int index) { + final colorScheme = Theme.of(context).colorScheme; + final onSurface = colorScheme.onSurface; + final surface = colorScheme.surface; + final outline = colorScheme.outlineVariant; + final warning = colorScheme.error; + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isDark + ? Color.fromARGB(255, 80, 80, 80) + : Color.fromARGB(255, 252, 252, 252), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Color.fromARGB(255, 207, 206, 205)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: AppColors.primaryColor[50], + shape: BoxShape.circle, + ), + child: Icon( + CupertinoIcons.person_solid, + color: AppColors.primaryColor[500], + size: 28, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + member['phone'] ?? '', + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.normal, + fontFamily: 'Dana', + color: onSurface, + ), + ), + const SizedBox(height: 4), + Text( + member['time'], + style: AppTextStyles.body6.copyWith( + color: onSurface.withOpacity(0.6), + fontSize: 11, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // InkWell( + // onTap: () { + // _showEditDialog(index); + // }, + // child: + // // Row( + // // children: [ + // // Text( + // // 'ویرایش', + // // style: AppTextStyles.body6.copyWith( + // // color: AppColors.primaryColor[500], + // // ), + // // ), + // // const SizedBox(width: 4), + // // SvgPicture.asset( + // // 'assets/icon/outline/edit.svg', + // // ), + // // ], + // // ), + // ), + const SizedBox(width: 16), + InkWell( + onTap: () { + _showDeleteDialog(index); + }, + child: SvgPicture.asset( + 'assets/icon/outline/trash.svg', + color: warning, + height: 25, + ), + ), + const SizedBox(width: 16), + // Container( + // padding: + // const EdgeInsets.symmetric(horizontal: 15, vertical: 9), + // decoration: BoxDecoration( + // color: Color.fromARGB(255, 224, 236, 255), + // borderRadius: BorderRadius.circular(6), + // ), + // child: Text( + // 'در انتظار تایید', + // style: AppTextStyles.body6.copyWith( + // color: colorScheme.primary, + // fontWeight: FontWeight.bold, + // fontSize: 10, + // ), + // ), + // ), + ], + ), + ], + ), + ); + } + + void _showEditDialog(int index) { + final member = _invitedMembers[index]; + final TextEditingController editPhoneController = + TextEditingController(text: member['phone']); + int? editAgeIndex = member['ageIndex']; + + showDialog( + context: context, + builder: (context) { + return StatefulBuilder( + builder: (context, setDialogState) { + return Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + decoration: BoxDecoration( + color: const Color.fromARGB(255, 246, 246, 246), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.gray[300]), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + vertical: 12, horizontal: 16), + decoration: const BoxDecoration( + color: Color.fromARGB(255, 224, 236, 255), + borderRadius: + BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Directionality( + textDirection: TextDirection.rtl, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'ویرایش عضو ${_getOrdinal(index + 1)}', + textAlign: TextAlign.right, + style: AppTextStyles.body4.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + InkWell( + onTap: () => Navigator.pop(context), + child: const Icon(CupertinoIcons.clear, size: 20), + ) + ], + ), + ), + ), + Container( + height: 2, + color: const Color.fromARGB(255, 30, 29, 27), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Directionality( + textDirection: TextDirection.rtl, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'شماره همراه', + style: AppTextStyles.body5.copyWith( + color: AppColors.primaryColor[500], + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + TextFormField( + controller: editPhoneController, + keyboardType: TextInputType.phone, + textDirection: TextDirection.ltr, + decoration: InputDecoration( + suffixIcon: Icon( + CupertinoIcons.phone, + color: Theme.of(context).colorScheme.primary, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: + BorderSide(color: AppColors.gray[300]), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: + BorderSide(color: AppColors.gray[300]), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: AppColors.primaryColor[500]), + ), + ), + ), + const SizedBox(height: 20), + Text( + 'رده سنی', + style: AppTextStyles.body5.copyWith( + color: AppColors.primaryColor[500], + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _buildDialogAgeOption( + title: 'زیر ۱۸ سال', + index: 0, + groupValue: editAgeIndex, + onChanged: (val) => setDialogState( + () => editAgeIndex = val), + ), + ), + const SizedBox(width: 8), + Expanded( + child: _buildDialogAgeOption( + title: '۱۸ تا ۳۰ سال', + index: 1, + groupValue: editAgeIndex, + onChanged: (val) => setDialogState( + () => editAgeIndex = val), + ), + ), + const SizedBox(width: 8), + Expanded( + child: _buildDialogAgeOption( + title: 'بالای ۳۰ سال', + index: 2, + groupValue: editAgeIndex, + onChanged: (val) => setDialogState( + () => editAgeIndex = val), + ), + ), + ], + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + if (editPhoneController.text.isNotEmpty) { + setState(() { + _invitedMembers[index] = { + 'phone': editPhoneController.text, + 'ageIndex': editAgeIndex, + 'time': member['time'], + }; + }); + Navigator.pop(context); + SnackBarManager(context).show( + message: 'اطلاعات با موفقیت ویرایش شد', + status: SnackBarStatus.success, + isTop: true, + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: + const Color.fromARGB(255, 34, 82, 160), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + padding: + const EdgeInsets.symmetric(vertical: 12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'ثبت تغییرات', + style: AppTextStyles.body4.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 8), + SvgPicture.asset( + 'assets/icon/outline/edit.svg', + color: Colors.white) + ], + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ); + }, + ); + } + + void _showDeleteDialog(int index) { + final member = _invitedMembers[index]; + showDialog( + context: context, + builder: (context) { + return Dialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Directionality( + textDirection: TextDirection.rtl, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + InkWell( + onTap: () => Navigator.pop(context), + child: SvgPicture.asset( + 'assets/icon/outline/close-circle.svg')), + ], + ), + SvgPicture.asset( + 'assets/icon/outline/trashPopup.svg', + width: 55, + height: 55, + ), + const SizedBox(height: 16), + Text( + 'کاربر حذف شود؟', + style: AppTextStyles.body3.copyWith( + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + const SizedBox(height: 8), + Text( + 'شماره همراه: ${member['phone']}', + style: AppTextStyles.body5.copyWith( + color: AppColors.black[700], + fontFamily: 'Dana', + ), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + backgroundColor: Color.fromARGB(255, 190, 18, 60), + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(100), + ), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + child: const Text( + 'خیر', + style: TextStyle( + fontFamily: 'Dana', + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () async { + final dynamic rawId = member['id']; + print( + 'Debug - Delete: rawId = $rawId (${rawId.runtimeType})'); + print('Debug - Delete: member = $member'); + + String? id; + + if (rawId is String) { + id = rawId; + } else if (rawId is int) { + id = rawId.toString(); + } + + if (id == null || id.isEmpty) { + Navigator.pop(context); + SnackBarManager(context).show( + message: + 'شناسه کاربر نامعتبر است: rawId=$rawId (type: ${rawId.runtimeType})', + status: SnackBarStatus.error, + isTop: true, + ); + return; + } + + print('Debug - Delete: parsed id = $id'); + + try { + print('Debug - Deleting user with id: $id'); + final isDeleted = + await AuthRepository.deleteSubUser(id); + + if (!mounted) return; + + if (isDeleted) { + setState(() { + _invitedMembers.removeAt(index); + }); + Navigator.pop(context); + SnackBarManager(context).show( + message: 'کاربر با موفقیت حذف شد', + status: SnackBarStatus.success, + isTop: true, + ); + } else { + Navigator.pop(context); + SnackBarManager(context).show( + message: 'خطا در حذف کاربر', + status: SnackBarStatus.error, + isTop: true, + ); + } + } catch (e) { + print('Debug - Delete error: $e'); + if (!mounted) return; + Navigator.pop(context); + SnackBarManager(context).show( + message: 'خطا در حذف کاربر: $e', + status: SnackBarStatus.error, + isTop: true, + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[200], + foregroundColor: Colors.black, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(100), + side: const BorderSide( + color: Color.fromARGB(255, 190, 18, 60), + ), + ), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + child: const Text( + 'بله', + style: TextStyle( + fontFamily: 'Dana', + ), + ), + ), + ), + ], + ) + ], + ), + ), + ), + ); + }, + ); + } + + Widget _buildDialogAgeOption({ + required String title, + required int index, + required int? groupValue, + required ValueChanged onChanged, + }) { + final colorScheme = Theme.of(context).colorScheme; + final isSelected = groupValue == index; + return InkWell( + onTap: () => onChanged(index), + borderRadius: BorderRadius.circular(8), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: isSelected + ? colorScheme.primaryContainer.withOpacity(0.35) + : colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(30), + ), + child: Text( + title, + textAlign: TextAlign.center, + style: AppTextStyles.body6.copyWith( + color: isSelected + ? colorScheme.onPrimaryContainer + : colorScheme.onSurface.withOpacity(0.7), + fontWeight: FontWeight.bold, + fontSize: 11, + ), + ), + ), + ); + } + + String _getOrdinal(int number) { + switch (number) { + case 1: + return 'اول'; + case 2: + return 'دوم'; + case 3: + return 'سوم'; + case 4: + return 'چهارم'; + case 5: + return 'پنجم'; + default: + return '$numberام'; + } + } + + Widget _buildInviteCard(int memberNumber) { + final colorScheme = Theme.of(context).colorScheme; + return Container( + decoration: BoxDecoration( + color: Color.fromARGB(255, 246, 246, 246), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ) + ], + ), + child: Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + decoration: BoxDecoration( + color: Color.fromARGB(255, 224, 236, 255), + borderRadius: + const BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Text( + 'دعوت از عضو ${_getOrdinal(memberNumber)}', + textAlign: TextAlign.right, + style: AppTextStyles.body4.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + Container( + height: 2, + color: colorScheme.outline, + ), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'شماره همراه', + style: AppTextStyles.body5.copyWith( + color: colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + TextFormField( + controller: _phoneController, + keyboardType: TextInputType.phone, + textDirection: TextDirection.ltr, + decoration: InputDecoration( + suffixIcon: Icon( + CupertinoIcons.phone, + color: Theme.of(context).colorScheme.primary, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: + BorderSide(color: Color.fromARGB(255, 161, 160, 160)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide(color: colorScheme.outlineVariant), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide(color: colorScheme.primary), + ), + ), + ), + const SizedBox(height: 20), + Text( + 'رده سنی', + style: AppTextStyles.body5.copyWith( + color: colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _buildAgeOption( + title: 'زیر ۱۸ سال', + index: 0, + ), + ), + const SizedBox(width: 8), + Expanded( + child: _buildAgeOption( + title: '۱۸ تا ۳۰ سال', + index: 1, + ), + ), + const SizedBox(width: 8), + Expanded( + child: _buildAgeOption( + title: 'بالای ۳۰ سال', + index: 2, + ), + ), + ], + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () async { + final phone = _phoneController.text; + + if (phone.isEmpty) { + SnackBarManager(context).show( + message: 'لطفا شماره همراه را وارد کنید', + status: SnackBarStatus.error, + isTop: true, + ); + return; + } + + if (_selectedAgeIndex == null) { + SnackBarManager(context).show( + message: 'لطفا رده سنی را انتخاب کنید', + status: SnackBarStatus.error, + isTop: true, + ); + return; + } + + int level; + switch (_selectedAgeIndex) { + case 0: + level = 3; + break; + case 1: + level = 2; + break; + case 2: + level = 1; + break; + default: + level = 3; + } + + try { + final isSuccess = + await AuthRepository.addSubUser(phone, level); + + if (isSuccess) { + if (!context.mounted) return; + + SnackBarManager(context).show( + message: 'دعوتنامه برای $phone با موفقیت ارسال شد', + status: SnackBarStatus.success, + isTop: true, + ); + + setState(() { + _showInviteForm = false; + _phoneController.clear(); + _selectedAgeIndex = null; + }); + await _fetchFamilyMembers(); + } + } catch (e) { + if (!context.mounted) return; + SnackBarManager(context).show( + message: + 'خطا در ارسال دعوتنامه. لطفا مجدد تلاش کنید.', + status: SnackBarStatus.error, + isTop: true, + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: colorScheme.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'ارسال دعوتنامه', + style: AppTextStyles.body4.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 8), + SvgPicture.asset('assets/icon/outline/send.svg') + ], + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildAgeOption({required String title, required int index}) { + final colorScheme = Theme.of(context).colorScheme; + final isSelected = _selectedAgeIndex == index; + return InkWell( + onTap: () { + setState(() { + _selectedAgeIndex = index; + }); + }, + borderRadius: BorderRadius.circular(8), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: isSelected + ? Color.fromARGB(255, 224, 236, 255) + : Color.fromARGB(255, 233, 232, 231), + borderRadius: BorderRadius.circular(30), + ), + child: Text( + title, + textAlign: TextAlign.center, + style: AppTextStyles.body6.copyWith( + color: isSelected + ? Colors.black + : colorScheme.onSurface.withOpacity(0.7), + fontWeight: FontWeight.bold, + fontSize: 11, + ), + ), + ), + ); + } + + Widget _buildTopInfoCard(BuildContext context, + {required String title, required String icon}) { + final colorScheme = Theme.of(context).colorScheme; + return Column( + children: [ + Container( + height: 64, + width: 64, + padding: const EdgeInsets.all(5), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Color.fromARGB(255, 248, 231, 241), + ), + child: Center( + child: SvgPicture.asset( + icon, + width: 32, + height: 32, + ), + ), + ), + const SizedBox(height: 8), + Text( + title, + style: AppTextStyles.body6.copyWith( + fontSize: 11, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ); + } + + Future _fetchFamilyMembers() async { + try { + final subUsers = await AuthRepository.getSubUsers(); + + setState(() { + _invitedMembers.clear(); + for (var user in subUsers) { + print('Debug - User data: $user'); + print('Debug - User ID: ${user['id']} (${user['id'].runtimeType})'); + + _invitedMembers.add({ + 'id': user['id'], + 'phone': user['mobile_number'], + 'ageIndex': _getAgeIndexFromLevel(user['level']), + 'time': _formatDate(user['created_at']), + }); + } + }); + print('Debug - Total members loaded: ${_invitedMembers.length}'); + } catch (e) { + print('Debug - Error fetching family members: $e'); + if (mounted) { + SnackBarManager(context).show( + message: 'خطا در دریافت اطلاعات خانواده: $e', + status: SnackBarStatus.error, + ); + } + } + } + + int _getAgeIndexFromLevel(int level) { + switch (level) { + case 3: + return 0; + case 2: + return 1; + case 1: + return 2; + default: + return 1; + } + } + + String _formatDate(String? isoDate) { + if (isoDate == null) return ''; + try { + DateTime date = DateTime.parse(isoDate); + DateTime now = DateTime.now(); + Duration difference = now.difference(date); + + if (difference.inSeconds < 60) { + return 'دعوت شده در ${difference.inSeconds} ثانیه قبل'; + } else if (difference.inMinutes < 60) { + return 'دعوت شده در ${difference.inMinutes} دقیقه قبل'; + } else if (difference.inHours < 24) { + return 'دعوت شده در ${difference.inHours} ساعت قبل'; + } else if (difference.inDays < 30) { + return 'دعوت شده در ${difference.inDays} روز قبل'; + } else if (difference.inDays < 365) { + int months = (difference.inDays / 30).floor(); + return 'دعوت شده در $months ماه قبل'; + } else { + int years = (difference.inDays / 365).floor(); + return 'دعوت شده در $years سال قبل'; + } + } catch (e) { + return isoDate; + } + } +} diff --git a/lib/ui/screens/faq/faq_page.dart b/lib/ui/screens/faq/faq_page.dart new file mode 100644 index 0000000..0a66462 --- /dev/null +++ b/lib/ui/screens/faq/faq_page.dart @@ -0,0 +1,108 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/dropdown/animated_setting_container.dart'; +import 'package:hoshan/ui/widgets/sections/header/reversible_appbar.dart'; + +class FaqPage extends StatefulWidget { + const FaqPage({super.key}); + + @override + State createState() => _FaqPageState(); +} + +class _FaqPageState extends State { + final list = [ + { + 'هوشان چیه و چی کار می‌کنه؟': + 'هوشان یک پلتفرم دستیار هوش مصنوعی است که برای کاربرانش این امکان را فراهم آورده است که به ارزان‌ترین و سریع‌ترین شکل ممکن به همه‌ی مدل‌ها و ابزارهای هوش مصنوعی در یک پلتفرم واحد دسترسی داشته باشند. به این خدمت اصطلاحاً (All in One) می‌گویند. علاوه بر این هوشان امکانات محنصر به فرد دیگری هم دارد. کاربران هوشان می‌توانند دستیارهای شخصی خودشان را بسازند یا دستیارهایی بسازند و آن را با دیگران به اشتراک بگذارند و به راحتی از آنها کسب درآمد کنند.' + }, + { + 'بات‌های هوش مصنوعی هوشان چین؟': + 'با استفاده از مهندسی پرامپت، دستیارها و بات‌هایی طراحی شد‌ه‌اند که هر کدام یک وظیفه خاص را به بهترین شکل ممکن انجام می‌دهند. هر بات در واقع یک پرامپت سفارشی شده است. کاربران هوشان هم می‌توانند دستیارهای شخصی خودشان را بسازند.' + }, + { + 'علاوه بر مدل‌های زبانی، هوشان امکانات دیگه‌ای هم داره؟': + 'بله، هوشان ابزارهای متنوعی برای تولید عکس، تولید صوت، تولید ویدیو و ترجمه دارد. این ابزارهای خارق‌العاده بهره‌وری شما را بسیار افزایش می‌دهند.' + }, + // { + // 'آیا ابزارهای هوشان رایگان هستن؟': + // 'ما پلن‌های رایگانی داریم که به شما امکان می‌دهد از برخی امکانات هوشان بدون محدودیت استفاده کنید، اما اگر به قابلیت‌های پیشرفته‌تر نیاز دارید، می‌توانید پلن‌هایی را انتخاب کنید که با قیمت مناسب و امکانات ویژه ارائه می‌شوند.' + // }, + { + 'امنیت اطلاعات من در هوشان چطور تضمین می‌شه؟': + 'ما به حریم خصوصی کاربران اهمیت زیادی می‌دهیم. شما به هیچ چیز جز یک شماره تلفن همراه برای ثبت نام در هوشان نیاز ندارید. علاوه بر اینکه می‌توانید با استفاده از ویژگی “حالت ناشناس” مطمئن بشوید که گفتگوهای شما با هوشان در هیچ جا ذخیره نمی‌شوند.' + }, + // { + // 'می‌تونیم دیگران را به هوشان دعوت کنیم؟': + // 'بله هر کاربر یک کد معرف دارد که می‌تواند با هر کسی آن را به اشتراک بگذارد، اگر آن شخص ثبت نام کند، علاوه بر اینکه مهمان شما 20 سکه هدیه دریافت می‌کند، خود شما هم به همان اندازه حسابتان شارژ می‌شود' + // } + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: ReversibleAppbar( + context, + title: Row( + children: [ + Text( + 'سوالات متداول', + style: AppTextStyles.body3 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ), + const SizedBox( + width: 8, + ), + Assets.icon.outline.messageQuestion.svg( + width: Responsive(context).isMobile() ? null : 32, + color: Theme.of(context).colorScheme.onSurface), + ], + ), + ), + body: Responsive(context).maxWidthInDesktop( + maxWidth: 800, + child: (contxet, mw) => SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + children: [ + const SizedBox( + height: 16, + ), + ...List.generate( + list.length, + (index) => AnimatedSettingContainer( + title: list[index].keys.first, + icon: Icons.filter_1_rounded, + childrens: [ + const SizedBox( + height: 24, + ), + Text( + list[index].values.first, + textDirection: TextDirection.rtl, + textAlign: TextAlign.justify, + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900]), + ), + const SizedBox( + height: 12, + ), + ]), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/ui/screens/gmedia/chats/audio_chat_page.dart b/lib/ui/screens/gmedia/chats/audio_chat_page.dart new file mode 100644 index 0000000..afcca7b --- /dev/null +++ b/lib/ui/screens/gmedia/chats/audio_chat_page.dart @@ -0,0 +1,972 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:math'; + +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/gen/my_flutter_app_icons.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/core/services/file_manager/download_file_services.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/data/model/ai/chats_history_model.dart'; +import 'package:hoshan/data/model/ai/messages_model.dart'; +import 'package:hoshan/data/model/ai/send_message_model.dart'; +import 'package:hoshan/ui/screens/chat/bloc/messages_bloc.dart'; +import 'package:hoshan/ui/screens/library/bloc/chats_history_bloc.dart'; +import 'package:hoshan/ui/screens/gmedia/cubit/media_g_response_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/audio/music_player.dart'; +import 'package:hoshan/ui/widgets/components/button/circle_icon_btn.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/dialog/dialog_handler.dart'; +import 'package:hoshan/ui/widgets/components/dropdown/hint_tooltip.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; +import 'package:share_plus/share_plus.dart'; + +class AudioChatPage extends StatefulWidget { + final String type; + final Bots bot; + final int? chatId; + final double maxWidth; + const AudioChatPage( + {super.key, + required this.type, + required this.bot, + this.chatId, + required this.maxWidth}); + + @override + State createState() => _AudioChatPageState(); +} + +class _AudioChatPageState extends State { + final FocusNode _textFieldFocus = FocusNode(); + final CarouselSliderController _carouselController = + CarouselSliderController(); + final ValueNotifier _currentIndex = ValueNotifier(3); + final TextEditingController _query = TextEditingController(); + final ValueNotifier isGhost = ValueNotifier(false); + final ValueNotifier maxSize = ValueNotifier(1); + + late int? chatId = widget.chatId; + late Bots bot = widget.bot; + + List> groupMessages(List messages) { + return messages.fold>>([], (acc, message) { + if (acc.isEmpty || + (acc.last.first.fromBot != message.fromBot && acc.last.length == 2) || + (acc.last.first.fromBot == message.fromBot && message.fromBot!) || + (acc.last.first.fromBot == message.fromBot && !message.fromBot!)) { + acc.add([message]); + } else { + acc.last.add(message); + } + return acc; + }); + } + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) { + final messagesBloc = MessagesBloc()..add(ResetMessages()); + if (chatId != null) { + messagesBloc.add(GetallMessages(chatId: chatId!)); + } + return messagesBloc; + }, + ), + ], + child: Scaffold( + floatingActionButton: ValueListenableBuilder( + valueListenable: _currentIndex, + builder: (context, value, _) { + return ValueListenableBuilder( + valueListenable: maxSize, + builder: (context, size, child) { + return value < size - 1 + ? Padding( + padding: const EdgeInsets.only(bottom: 64.0), + child: FloatingActionButton.small( + shape: const CircleBorder(), + onPressed: () { + _carouselController.animateToPage(size - 1); + }, + child: Transform.rotate( + angle: pi / 2, + child: Assets.icon.outline.arrowRight.svg( + color: Colors.white, + ), + ), + )) + : const SizedBox.shrink(); + }, + ); + }), + bottomSheet: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + widget.type == 'file' + ? Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: LoadingButton( + width: double.infinity, + onPressed: () async { + if (context.read().state + is MediaGResponseLoading) { + return; + } + FilePickerResult? result = + await FilePicker.platform.pickFiles( + type: FileType.audio, + ); + if (result != null) { + if (mounted) { + context + .read() + .request(SendMessageModel( + id: chatId, + ghost: isGhost.value, + messageId: + DateTime.now().toIso8601String(), + file: result.xFiles.single, + botId: bot.id, + )); + } + } + }, + backgroundColor: + Theme.of(context).colorScheme.primary, + child: Text( + 'بارگذاری فایل صوتی', + style: AppTextStyles.body4 + .copyWith(color: Colors.white), + )), + ), + ) + : Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Directionality( + textDirection: TextDirection.rtl, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + CircleIconBtn( + icon: Assets.icon.bold.send, + color: Theme.of(context).colorScheme.primary, + iconColor: Colors.white, + iconPadding: const EdgeInsets.all(6), + size: 26, + onTap: () { + if (context.read().state + is MediaGResponseLoading) { + return; + } + context + .read() + .request(SendMessageModel( + messageId: + DateTime.now().toIso8601String(), + id: chatId, + ghost: isGhost.value, + query: _query.text, + botId: bot.id, + )); + _query.clear(); + }, + ), + const SizedBox( + width: 8, + ), + Expanded( + child: TextField( + controller: _query, + focusNode: _textFieldFocus, + onTapOutside: (event) { + _textFieldFocus.unfocus(); + }, + minLines: widget.type == 'music' ? 4 : 1, + maxLines: 4, + keyboardType: TextInputType.multiline, + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + decoration: InputDecoration.collapsed( + hintText: + 'متن ${widget.type == 'music' ? 'ترانه ' : ''}را وارد کنید...', + hintStyle: AppTextStyles.body4.copyWith( + color: AppColors.gray[context + .read() + .isDark() + ? 600 + : 900])), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + body: Stack( + children: [ + Assets.image.audioBack.image( + width: widget.maxWidth, + height: MediaQuery.sizeOf(context).height, + // color: Theme.of(context).scaffoldBackgroundColor, + // colorBlendMode: BlendMode.multiply, + opacity: AlwaysStoppedAnimation( + context.read().isDark() ? 0.4 : 0.8), + fit: BoxFit.cover, + ), + Positioned.fill(child: BlocBuilder( + builder: (context, mState) { + if (mState is MessagesFail) { + return const SizedBox(); + } + if (mState is MessagesLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + final m = mState.messages; + List> messages = groupMessages(m); + return BlocConsumer( + listener: (context, state) async { + if (state is MediaGResponseLoading) { + await Future.delayed(const Duration(milliseconds: 600)); + _carouselController.animateToPage(maxSize.value); + } + if (state is MediaGResponseFail) { + SnackBarManager(context).show( + message: + 'خطا از طرف سرور لطفا لحظاتی دیگر دوباره تلاش کنید', + status: SnackBarStatus.error); + } + if (state is MediaGResponseSucess) { + context.read().add(AddMessage( + message: Messages( + query: state.query, + file: state.file, + createdAt: DateTime.now().toIso8601String(), + error: state.response.error, + id: state.response.humanMessageId, + role: 'user'))); + if (!(state.response.error ?? true)) { + context.read().add(AddMessage( + message: Messages( + content: [ + Content( + audioUrl: + FileUrl(url: state.response.content)) + ], + createdAt: DateTime.now().toIso8601String(), + error: state.response.error, + id: state.response.aiMessageId, + role: 'ai'))); + } + if (chatId == null && !isGhost.value) { + context.read().add(AddChat( + chats: Chats( + bot: bot, + title: state.response.chatTitle, + createdAt: DateTime.now().toIso8601String(), + id: state.response.chatId))); + } + + chatId = state.response.chatId; + } + }, + builder: (context, state) { + maxSize.value = messages.length + + 1 + + (state is MediaGResponseLoading ? 1 : 0); + return CarouselSlider.builder( + carouselController: _carouselController, + itemCount: messages.length + + 1 + + (state is MediaGResponseLoading ? 1 : 0), + options: CarouselOptions( + initialPage: 3, + viewportFraction: 1, + enlargeFactor: 0.1, + height: MediaQuery.sizeOf(context).height, + autoPlay: false, + scrollDirection: Axis.vertical, + enableInfiniteScroll: false, + onPageChanged: (index, reason) { + _currentIndex.value = index; + }), + itemBuilder: (context, index, realIndex) { + if (state is MediaGResponseLoading && + index == messages.length + 1) { + final yourScrollController = ScrollController(); + + return Column( + children: [ + Flexible( + flex: 1, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + widget.type == 'file' + ? Container( + width: widget.maxWidth * 0.8, + padding: + const EdgeInsets.all(16), + margin: + const EdgeInsets.all(16), + decoration: BoxDecoration( + color: context + .read< + ThemeModeCubit>() + .isDark() + ? AppColors.black[900] + : Colors.white, + borderRadius: BorderRadius + .circular(16) + .copyWith( + bottomLeft: + Radius.zero)), + child: MusicPlayer( + url: state.file!.path)) + : Container( + width: widget.maxWidth * 0.8, + constraints: BoxConstraints( + maxHeight: + MediaQuery.sizeOf( + context) + .height * + 0.3), + padding: + const EdgeInsets.all(16), + margin: + const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .primary, + borderRadius: BorderRadius + .circular(16) + .copyWith( + bottomRight: + Radius.zero)), + child: Directionality( + textDirection: + TextDirection.rtl, + child: textLay( + yourScrollController, + state.query ?? ''), + ), + ), + ], + )), + Flexible( + child: Column( + children: [ + loading(context), + ], + )) + ], + ); + } else if (index != 0) { + final ms = messages[index - 1]; + Messages? user; + Messages? ai; + if (ms.length == 2) { + user = ms.first; + ai = ms.last; + } else if (ms.length == 1) { + if (ms.single.fromBot ?? false) { + ai = ms.single; + } else { + user = ms.single; + } + } + final yourScrollController = ScrollController(); + return Column( + children: [ + const SizedBox( + height: 16, + ), + if (user != null) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + widget.type == 'file' + ? Container( + width: widget.maxWidth * 0.8, + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: context + .read< + ThemeModeCubit>() + .isDark() + ? AppColors.black[900] + : Colors.white, + borderRadius: + BorderRadius.circular(16) + .copyWith( + bottomLeft: + Radius.zero)), + child: MusicPlayer( + url: user.file!.path)) + : Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Container( + width: widget.maxWidth * 0.8, + constraints: BoxConstraints( + maxHeight: + MediaQuery.sizeOf( + context) + .height * + 0.3), + padding: + const EdgeInsets.all(16), + margin: + const EdgeInsets.all(16) + .copyWith(bottom: 8), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .primary, + borderRadius: BorderRadius + .circular(16) + .copyWith( + bottomRight: + Radius.zero)), + child: Directionality( + textDirection: + TextDirection.rtl, + child: textLay( + yourScrollController, + user.query ?? ''), + ), + ), + Padding( + padding: + const EdgeInsets.only( + left: 18.0, + bottom: 16), + child: Row( + mainAxisAlignment: + MainAxisAlignment.start, + children: [ + CircleIconBtn( + color: Theme.of(context) + .colorScheme + .primary, + iconColor: Colors.white, + icon: Assets + .icon.outline.copy, + onTap: () async { + try { + await Clipboard.setData( + ClipboardData( + text: user! + .query!)); + Future.delayed( + Duration.zero, + () => SnackBarManager( + context, + id: + 'Copy') + .show( + message: + 'متن کپی شد 😃')); + } catch (e) { + if (kDebugMode) { + print(e); + } + } + }, + ), + ], + ), + ), + ], + ), + ], + ), + if (user?.error ?? false) + error( + context, + () { + context.read().add( + DeleteMessageWithId( + messageId: user!.id!)); + context + .read() + .request(SendMessageModel( + id: chatId, + query: widget.type != 'file' + ? null + : _query.text, + file: widget.type == 'file' + ? user.file + : null, + botId: bot.id, + ghost: isGhost.value, + messageId: DateTime.now() + .toIso8601String(), + )); + }, + ), + if (ai != null) + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + widget.type == 'file' + ? Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + Container( + width: widget.maxWidth * 0.8, + constraints: BoxConstraints( + maxHeight: + MediaQuery.sizeOf( + context) + .height * + 0.3), + padding: + const EdgeInsets.all(16), + margin: + const EdgeInsets.all(16) + .copyWith(bottom: 8), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .primary, + borderRadius: BorderRadius + .circular(16) + .copyWith( + bottomRight: + Radius.zero)), + child: Directionality( + textDirection: + TextDirection.rtl, + child: textLay( + yourScrollController, + ai + .content + ?.first + .audioUrl + ?.url ?? + ''), + ), + ), + Padding( + padding: + const EdgeInsets.only( + right: 18.0, + bottom: 16), + child: Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + CircleIconBtn( + color: Theme.of(context) + .colorScheme + .primary, + iconColor: Colors.white, + icon: Assets + .icon.outline.copy, + onTap: () async { + try { + await Clipboard.setData( + ClipboardData( + text: ai! + .content! + .first + .audioUrl! + .url!)); + } catch (e) { + if (kDebugMode) { + print(e); + } + } + }, + ), + ], + ), + ) + ], + ) + : Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + Container( + width: widget.maxWidth * 0.8, + padding: + const EdgeInsets.all(16), + margin: + const EdgeInsets.all(16) + .copyWith(bottom: 8), + decoration: BoxDecoration( + color: context + .read< + ThemeModeCubit>() + .isDark() + ? AppColors.black[900] + : Colors.white, + borderRadius: BorderRadius + .circular(16) + .copyWith( + bottomLeft: + Radius.zero)), + child: MusicPlayer( + url: ai.content?.first + .audioUrl?.url ?? + ''), + ), + Padding( + padding: + const EdgeInsets.only( + right: 18.0, + bottom: 16), + child: Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + CircleIconBtn( + color: Theme.of(context) + .colorScheme + .primary, + iconColor: Colors.white, + icon: Assets.icon + .outline.download, + onTap: () { + try { + DownloadFileService.getFile( + url: ai! + .content! + .first + .audioUrl! + .url!) + .then((value) { + SnackBarManager(context).show( + message: + 'فایل با موفقیت در پوشه Downloads نشست.', + status: SnackBarStatus + .success); + }); + } catch (e) { + if (kDebugMode) { + print(e); + } + } + }, + ), + const SizedBox( + width: 8, + ), + CircleIconBtn( + color: Theme.of(context) + .colorScheme + .primary, + iconColor: Colors.white, + icon: Assets + .icon.outline.share, + onTap: () async { + try { + await Share.share(ai! + .content! + .first + .audioUrl! + .url + .toString()); + } catch (e) { + if (kDebugMode) { + print( + 'Error in share Text: $e'); + } + } + }, + ), + ], + ), + ) + ], + ), + ], + ), + ], + ); + } + return Stack( + children: [ + Column( + children: [ + const SizedBox( + height: 16, + ), + if (bot.description != null) + Container( + margin: const EdgeInsets.symmetric( + horizontal: 16), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(16), + color: Theme.of(context) + .colorScheme + .surface), + child: Text( + bot.description!, + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + textDirection: TextDirection.rtl, + textAlign: TextAlign.justify, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + ValueListenableBuilder( + valueListenable: isGhost, + builder: (context, g, _) { + return Transform.scale( + scale: 0.8, + child: Switch.adaptive( + value: g, + thumbIcon: + WidgetStateProperty + .resolveWith< + Icon?>( + (Set + states) { + if (states.contains( + WidgetState + .selected)) { + return Icon( + CustomIcons + .ghost, + color: Theme.of( + context) + .colorScheme + .onSurface); + } + return Icon( + Icons.close, + color: Theme.of( + context) + .colorScheme + .onSurface); + }, + ), + onChanged: (value) { + isGhost.value = value; + }, + ), + ); + }), + const SizedBox( + width: 8, + ), + Text( + 'حالت ناشناس', + style: AppTextStyles.body4 + .copyWith( + color: Theme.of(context) + .colorScheme + .onSurface, + fontWeight: + FontWeight.bold), + ), + const SizedBox( + width: 8, + ), + Padding( + padding: const EdgeInsets.only( + bottom: 4.0), + child: HintTooltip( + hint: + 'با فعال کردن این گزینه؛ چت‌های شما در قسمت تاریخچه، ذخیره نمی‌شوند و اطلاعاتتان ناشناس باقی می‌ماند.', + iconColor: Theme.of(context) + .colorScheme + .onSurface, + ), + ) + ], + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .secondary, + borderRadius: + BorderRadius.circular(12)), + child: Row( + children: [ + Text( + bot.cost == 0 || + bot.cost == null + ? 'رایگان' + : bot.cost.toString(), + style: AppTextStyles.body3 + .copyWith( + color: Colors.white), + ), + const SizedBox( + width: 4, + ), + Assets.icon.outline.coin.svg( + color: Colors.white, + width: 18, + height: 18) + ], + ), + ), + ], + ), + ), + const SizedBox( + height: 32, + ), + ], + ), + // Positioned( + // left: 16, + // bottom: widget.type == 'music' ? 150 : 74, + // child: CircleIconBtn( + // onTap: () { + // DialogHandler(context: context) + // .onMusicCreate(); + // }, + // size: 32, + // icon: Assets.icon.outline.idea)) + ], + ); + }); + }, + ); + }, + )) + ], + ), + ), + ); + } + + Scrollbar textLay(ScrollController yourScrollController, String text) { + return Scrollbar( + thumbVisibility: true, + trackVisibility: true, + interactive: true, + controller: yourScrollController, + radius: const Radius.circular(16), + child: SingleChildScrollView( + controller: yourScrollController, + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: SelectableText( + text, + + style: AppTextStyles.body4.copyWith( + color: Colors.white, + ), + // overflow: TextOverflow.ellipsis, + // textAlign: TextAlign.justify, + ), + ), + ), + ); + } + + Widget error(BuildContext context, Function()? onRetry) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: AppColors.red.defaultShade), + child: Center( + child: Text( + 'خطا لطفا مجددا تلاش کنید', + style: AppTextStyles.body5 + .copyWith(color: Colors.white, fontWeight: FontWeight.bold), + ), + ), + ), + const SizedBox( + height: 8, + ), + // LoadingButton( + // color: AppColors.red.defaultShade, + // onPressed:(){ + // context.go(Routes.purchase); + // }, + // child: Text( + // 'افزایش اعتبار', + // style: AppTextStyles.body4.copyWith(color: Colors.white), + // ), + // ) + ], + ); + } + + Container loading(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: context.read().isDark() + ? AppColors.black[900] + : AppColors.secondryColor[50]), + child: Row( + children: [ + Expanded( + child: SpinKitThreeBounce( + size: 32, + color: Theme.of(context).colorScheme.secondary, + )), + Text( + 'این کار ممکن است کمی طول بکشد', + style: AppTextStyles.body5 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ) + ], + ), + ); + } +} diff --git a/lib/ui/screens/gmedia/chats/photo_chat_page.dart b/lib/ui/screens/gmedia/chats/photo_chat_page.dart new file mode 100644 index 0000000..95fc6d8 --- /dev/null +++ b/lib/ui/screens/gmedia/chats/photo_chat_page.dart @@ -0,0 +1,1036 @@ +// ignore_for_file: use_build_context_synchronously, avoid_print + +import 'dart:math'; + +import 'package:before_after/before_after.dart'; +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/gen/my_flutter_app_icons.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/core/services/file_manager/download_file_services.dart'; +import 'package:hoshan/data/model/ai/chats_history_model.dart'; +import 'package:hoshan/data/model/ai/messages_model.dart'; +import 'package:hoshan/data/model/ai/send_message_model.dart'; +import 'package:hoshan/data/model/chat_args.dart'; +import 'package:hoshan/data/storage/shared_preferences_helper.dart'; +import 'package:hoshan/ui/screens/chat/bloc/messages_bloc.dart'; +import 'package:hoshan/ui/screens/library/bloc/chats_history_bloc.dart'; +import 'package:hoshan/ui/screens/library/library_screen.dart'; +import 'package:hoshan/ui/screens/gmedia/cubit/media_g_response_cubit.dart'; +import 'package:hoshan/ui/screens/gmedia/send_image_modal.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/circle_icon_btn.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/dialog/dialog_handler.dart'; +import 'package:hoshan/ui/widgets/components/dropdown/hint_tooltip.dart'; +import 'package:hoshan/ui/widgets/components/image/custome_image.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; +import 'package:share_plus/share_plus.dart'; + +class PhotoChatPage extends StatefulWidget { + final ChatArgs chatArgs; + const PhotoChatPage({super.key, required this.chatArgs}); + + @override + State createState() => _PhotoChatPageState(); +} + +class _PhotoChatPageState extends State { + final CarouselSliderController _carouselController = + CarouselSliderController(); + final TextEditingController _query = TextEditingController(); + final ValueNotifier _currentIndex = ValueNotifier(0); + final ValueNotifier _comp = ValueNotifier(0.5); + final ValueNotifier _compBot = ValueNotifier(0.5); + final FocusNode _textFieldFocus = FocusNode(); + bool inComp = false; + late final ptp = + widget.chatArgs.bot.attachmentType?.contains('image') ?? false; + final ValueNotifier isGhost = ValueNotifier(false); + + late final bot = widget.chatArgs.bot; + late int? chatId = widget.chatArgs.chatId; + final ValueNotifier maxSize = ValueNotifier(1); + + List> groupMessages(List messages) { + return messages.fold>>([], (acc, message) { + if (acc.isEmpty || + (acc.last.first.fromBot != message.fromBot && acc.last.length == 2) || + (acc.last.first.fromBot == message.fromBot && message.fromBot!) || + (acc.last.first.fromBot == message.fromBot && !message.fromBot!)) { + acc.add([message]); + } else { + acc.last.add(message); + } + return acc; + }); + } + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().add(RestartChatsHistory()); + + context.read().add(const GetAllChats(type: 'image')); + + if (!GuidsStorage.isSeenImage()) { + DialogHandler(context: context).onPhotoCreated(); + GuidsStorage.setSeenImage(true); + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Responsive(context).maxWidthInDesktop( + child: (contxet, mw) => Scaffold( + appBar: AppBar(), + drawer: Drawer( + shape: + const BeveledRectangleBorder(borderRadius: BorderRadius.zero), + child: LibraryScreen( + type: 'image', + onTap: (chat) { + context.push(Routes.photoToPhoto, + extra: ChatArgs(bot: chat.bot!, chatId: chat.id)); + }, + )), + bottomSheet: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: ptp + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: LoadingButton( + width: double.infinity, + onPressed: () { + Navigator.of(context).push(SendImageModal( + onFileSelected: (image) { + if (context + .read() + .state is MediaGResponseLoading) { + return; + } + context.read().request( + SendMessageModel( + ghost: isGhost.value, + messageId: DateTime.now() + .toIso8601String(), + file: image.xFile, + botId: widget.chatArgs.bot.id, + id: chatId)); + }, + )); + }, + backgroundColor: + Theme.of(context).colorScheme.primary, + child: Text( + 'بارگذاری عکس', + style: AppTextStyles.body4 + .copyWith(color: Colors.white), + )), + ) + : Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Directionality( + textDirection: TextDirection.rtl, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + CircleIconBtn( + icon: Assets.icon.bold.send, + color: Theme.of(context).colorScheme.primary, + iconColor: Colors.white, + iconPadding: const EdgeInsets.all(6), + size: 26, + onTap: () { + if (context + .read() + .state is MediaGResponseLoading) { + return; + } + context.read().request( + SendMessageModel( + messageId: DateTime.now() + .toIso8601String(), + query: _query.text, + botId: widget.chatArgs.bot.id, + ghost: isGhost.value, + id: chatId)); + + _query.clear(); + }, + ), + const SizedBox( + width: 8, + ), + Flexible( + child: TextField( + controller: _query, + focusNode: _textFieldFocus, + onTapOutside: (event) { + _textFieldFocus.unfocus(); + }, + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + decoration: InputDecoration.collapsed( + hintText: + 'تصویری که می‌خوای رو توصیف کن...', + hintStyle: AppTextStyles.body4.copyWith( + color: AppColors.gray[context + .read() + .isDark() + ? 600 + : 900])), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + floatingActionButton: ValueListenableBuilder( + valueListenable: _currentIndex, + builder: (context, value, _) { + return ValueListenableBuilder( + valueListenable: maxSize, + builder: (context, size, child) { + return value < size - 1 + ? Padding( + padding: const EdgeInsets.only(bottom: 64.0), + child: FloatingActionButton.small( + shape: const CircleBorder(), + onPressed: () { + _carouselController.animateToPage(size - 1); + }, + child: Transform.rotate( + angle: pi / 2, + child: Assets.icon.outline.arrowRight.svg( + color: Colors.white, + ), + ), + )) + : const SizedBox.shrink(); + }, + ); + }), + body: Stack( + children: [ + Assets.image.imageGBack.image( + width: MediaQuery.sizeOf(context).width, + height: MediaQuery.sizeOf(context).height, + color: + Theme.of(context).scaffoldBackgroundColor.withAlpha(240), + colorBlendMode: BlendMode.multiply, + fit: BoxFit.cover, + opacity: AlwaysStoppedAnimation( + context.read().isDark() ? 0.5 : 0.2)), + Positioned.fill(child: BlocBuilder( + builder: (context, mState) { + if (mState is MessagesFail) { + return const SizedBox(); + } + if (mState is MessagesLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + // if (state is MessagesSuccess) { + final m = mState.messages; + List> messages = groupMessages(m); + + return BlocConsumer( + listener: (context, state) async { + if (state is MediaGResponseSucess) { + context.read().add(AddMessage( + message: Messages( + query: state.query, + file: state.file, + createdAt: DateTime.now().toIso8601String(), + error: state.response.error, + id: state.response.humanMessageId, + role: 'user'))); + if (!(state.response.error ?? true)) { + context.read().add(AddMessage( + message: Messages( + content: [ + Content( + imageUrl: FileUrl( + url: state.response.content)) + ], + createdAt: + DateTime.now().toIso8601String(), + error: state.response.error, + id: state.response.aiMessageId, + role: 'ai'))); + } + if (chatId == null && !isGhost.value) { + context.read().add(AddChat( + chats: Chats( + bot: bot, + title: state.response.chatTitle, + createdAt: DateTime.now().toIso8601String(), + id: state.response.chatId))); + } + + chatId = state.response.chatId; + } + }, + builder: (context, state) { + maxSize.value = messages.length + 1; + print('📊 Messages count: ${messages.length}'); + print('📊 Bot name: ${bot.name}'); + return ListView( + physics: const BouncingScrollPhysics(), + children: [ + // Bot info section + Column( + children: [ + const SizedBox( + height: 16, + ), + ptp + ? ValueListenableBuilder( + valueListenable: _compBot, + builder: (context, val, child) => + SizedBox( + width: 86 * 2, + height: 100 * 2, + child: BeforeAfter( + trackWidth: 4, + thumbWidth: 18, + value: val, + onValueChanged: (value) => + _compBot.value = value, + before: AspectRatio( + aspectRatio: 3 / 4, + child: ImageNetwork( + width: double.infinity, + height: double.infinity, + radius: 12, + showHero: true, + url: bot.image2 ?? '')), + after: AspectRatio( + aspectRatio: 3 / 4, + child: ImageNetwork( + width: double.infinity, + height: double.infinity, + radius: 12, + showHero: true, + url: bot.image ?? '')), + ), + )) + : ImageNetwork( + width: 86, + height: 100, + url: bot.image ?? ''), + const SizedBox( + height: 8, + ), + Text( + bot.name ?? '', + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + const SizedBox( + height: 8, + ), + if (bot.description != null) + Container( + margin: const EdgeInsets.symmetric( + horizontal: 16), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context) + .colorScheme + .surface), + child: Text( + bot.description!, + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + textDirection: TextDirection.rtl, + textAlign: TextAlign.justify, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + ValueListenableBuilder( + valueListenable: isGhost, + builder: (context, g, _) { + return Transform.scale( + scale: 0.8, + child: Switch.adaptive( + value: g, + thumbIcon: WidgetStateProperty + .resolveWith( + (Set + states) { + if (states.contains( + WidgetState.selected)) { + return Icon( + CustomIcons.ghost, + color: + Theme.of(context) + .colorScheme + .onSurface); + } + return Icon(Icons.close, + color: Theme.of(context) + .colorScheme + .onSurface); + }), + onChanged: (value) { + isGhost.value = value; + }, + ), + ); + }), + const SizedBox( + width: 8, + ), + Text( + 'حالت ناشناس', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface, + fontWeight: FontWeight.bold), + ), + const SizedBox( + width: 8, + ), + Padding( + padding: const EdgeInsets.only( + bottom: 4.0), + child: HintTooltip( + hint: + 'با فعال کردن این گزینه؛ چت‌های شما در قسمت تاریخچه، ذخیره نمی‌شوند و اطلاعاتتان ناشناس باقی می‌ماند.', + iconColor: Theme.of(context) + .colorScheme + .onSurface, + ), + ) + ], + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .secondary, + borderRadius: + BorderRadius.circular(12)), + child: Row( + children: [ + Text( + bot.cost == 0 || bot.cost == null + ? 'رایگان' + : bot.cost.toString(), + style: AppTextStyles.body3 + .copyWith(color: Colors.white), + ), + const SizedBox( + width: 4, + ), + Assets.icon.outline.coin.svg( + color: Colors.white, + width: 18, + height: 18) + ], + ), + ), + ], + ), + ), + const SizedBox( + height: 16, + ), + ], + ), + // Messages list + ...messages.asMap().entries.map((entry) { + final index = entry.key; + final ms = entry.value; + print('🔵 Building message index: $index'); + final yourScrollController = ScrollController(); + Messages? user; + Messages? ai; + if (ms.length == 2) { + user = ms.first; + ai = ms.last; + } else if (ms.length == 1) { + if (ms.single.fromBot ?? false) { + ai = ms.single; + } else { + user = ms.single; + } + } + return inComp + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ValueListenableBuilder( + valueListenable: _comp, + builder: (context, val, child) => + BeforeAfter( + trackWidth: 4, + thumbWidth: 24, + value: val, + onValueChanged: (value) => + _comp.value = value, + before: Container( + width: mw * 0.6, + padding: + const EdgeInsets.all(4), + decoration: BoxDecoration( + color: context + .read< + ThemeModeCubit>() + .isDark() + ? AppColors.black[900] + : AppColors + .primaryColor[50], + borderRadius: + BorderRadius.circular( + 16)), + child: AspectRatio( + aspectRatio: 3 / 4, + child: user!.file != null + ? ClipRRect( + borderRadius: + BorderRadius + .circular( + 12), + child: CustomeImage( + src: user + .file!.path, + fit: BoxFit.cover, + )) + : ImageNetwork( + width: + double.infinity, + height: + double.infinity, + radius: 12, + showHero: true, + url: user + .content + ?.first + .imageUrl + ?.url ?? + ''), + ), + ), + after: Container( + width: mw * 0.6, + padding: + const EdgeInsets.all(4), + decoration: BoxDecoration( + color: context + .read< + ThemeModeCubit>() + .isDark() + ? AppColors.black[900] + : AppColors + .primaryColor[50], + borderRadius: + BorderRadius.circular( + 16)), + child: AspectRatio( + aspectRatio: 3 / 4, + child: ImageNetwork( + width: + double.infinity, + height: + double.infinity, + radius: 12, + showHero: true, + url: ai! + .content + ?.first + .imageUrl + ?.url ?? + '')), + ), + )), + SizedBox( + height: 16, + ), + CircleIconBtn( + size: 32, + color: Theme.of(context) + .colorScheme + .primary, + iconColor: Colors.white, + onTap: () { + setState(() { + inComp = !inComp; + }); + }, + icon: + Assets.icon.outline.bitcoinRefresh, + ), + ], + ) + : Column( + children: [ + const SizedBox( + height: 16, + ), + if (user != null) + Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + Container( + width: mw * 0.6, + padding: const EdgeInsets.all(4), + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: user.error ?? false + ? AppColors + .red.defaultShade + : Theme.of(context) + .colorScheme + .primary, + borderRadius: + BorderRadius.circular(16) + .copyWith( + bottomRight: + Radius.zero)), + child: ptp + ? AspectRatio( + aspectRatio: 3 / 4, + child: user.file != null + ? ClipRRect( + borderRadius: + BorderRadius + .circular( + 12), + child: + CustomeImage( + src: user + .file!.path, + fit: BoxFit + .cover, + )) + : ImageNetwork( + width: double + .infinity, + height: double + .infinity, + radius: 12, + showHero: true, + url: user + .content + ?.first + .imageUrl + ?.url ?? + ''), + ) + : Padding( + padding: + const EdgeInsets.all( + 16.0), + child: Directionality( + textDirection: + TextDirection.rtl, + child: Scrollbar( + thumbVisibility: true, + trackVisibility: true, + interactive: true, + controller: + yourScrollController, + radius: const Radius + .circular(16), + child: + SingleChildScrollView( + controller: + yourScrollController, + physics: + const BouncingScrollPhysics(), + child: Padding( + padding: + const EdgeInsets + .only( + left: + 8.0), + child: + SelectableText( + user.query ?? + '', + + style: + AppTextStyles + .body4 + .copyWith( + color: Colors + .white, + ), + // overflow: TextOverflow.ellipsis, + // textAlign: TextAlign.justify, + ), + ), + ), + ), + ), + ), + ), + ], + ), + if (ai != null && + user != null && + state is! MediaGResponseLoading && + ptp) + CircleIconBtn( + color: Theme.of(context) + .colorScheme + .primary, + iconColor: Colors.white, + onTap: () { + setState(() { + inComp = !inComp; + }); + }, + icon: Assets + .icon.outline.bitcoinRefresh, + ), + if (user?.error ?? false) + error( + context, + () { + context.read().add( + DeleteMessageWithId( + messageId: user!.id!)); + context + .read() + .request(SendMessageModel( + id: chatId, + query: + ptp ? null : _query.text, + file: ptp ? user.file : null, + botId: widget.chatArgs.bot.id, + ghost: isGhost.value, + messageId: DateTime.now() + .toIso8601String(), + )); + }, + ), + if (ai != null) + Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + Container( + width: + MediaQuery.sizeOf(context) + .width * + (ptp ? 0.6 : 0.7), + padding: + const EdgeInsets.all(4), + margin: + const EdgeInsets.all(16) + .copyWith(bottom: 8), + decoration: BoxDecoration( + color: context + .read< + ThemeModeCubit>() + .isDark() + ? AppColors.black[900] + : AppColors + .secondryColor[ + 50], + borderRadius: BorderRadius + .circular(16) + .copyWith( + bottomLeft: + Radius.zero)), + child: AspectRatio( + aspectRatio: 3 / 4, + child: ImageNetwork( + width: double.infinity, + height: double.infinity, + radius: 12, + showHero: true, + url: ai + .content + ?.first + .imageUrl + ?.url ?? + ''), + ), + ), + if (ptp) + Expanded( + child: Padding( + padding: + const EdgeInsets.only( + bottom: 16.0), + child: Row( + mainAxisAlignment: + MainAxisAlignment + .start, + crossAxisAlignment: + CrossAxisAlignment + .end, + children: [ + CircleIconBtn( + color: + Theme.of(context) + .colorScheme + .primary, + iconColor: + Colors.white, + icon: Assets.icon + .outline.download, + onTap: () { + try { + DownloadFileService.getFile( + url: ai! + .content! + .first + .imageUrl! + .url!) + .then( + (value) { + SnackBarManager( + context) + .show( + message: + 'فایل با موفقیت در پوشه Downloads نشست.', + status: + SnackBarStatus.success); + }); + } catch (e) { + if (kDebugMode) { + print(e); + } + } + }, + ), + const SizedBox( + width: 8, + ), + CircleIconBtn( + color: + Theme.of(context) + .colorScheme + .primary, + iconColor: + Colors.white, + icon: Assets.icon + .outline.share, + onTap: () async { + try { + await Share.share(ai! + .content! + .first + .imageUrl! + .url + .toString()); + } catch (e) { + if (kDebugMode) { + print( + 'Error in share Text: $e'); + } + } + }, + ), + ], + ), + )), + ], + ), + if (!ptp) + Padding( + padding: EdgeInsets.only( + right: MediaQuery.sizeOf( + context) + .width * + 0.26), + child: Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + CircleIconBtn( + color: Theme.of(context) + .colorScheme + .primary, + iconColor: Colors.white, + icon: Assets.icon.outline + .download, + onTap: () { + try { + DownloadFileService.getFile( + url: ai! + .content! + .first + .imageUrl! + .url!) + .then((value) { + SnackBarManager(context).show( + message: + 'فایل با موفقیت در پوشه Downloads نشست.', + status: + SnackBarStatus + .success); + }); + } catch (e) { + if (kDebugMode) { + print(e); + } + } + }, + ), + const SizedBox( + width: 8, + ), + CircleIconBtn( + color: Theme.of(context) + .colorScheme + .primary, + iconColor: Colors.white, + icon: Assets + .icon.outline.share, + onTap: () async { + try { + await Share.share(ai! + .content! + .first + .imageUrl! + .url + .toString()); + } catch (e) { + if (kDebugMode) { + print( + 'Error in share Text: $e'); + } + } + }, + ), + ], + ), + ) + ], + ), + const SizedBox( + height: 90, + ) + ], + ); + }).toList(), + const SizedBox(height: 100), + ], + ); + }, + ); + // } + }, + )), + ], + ), + ), + ), + ); + } + + Container loading(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: context.read().isDark() + ? AppColors.black[900] + : AppColors.secondryColor[50]), + child: Row( + children: [ + Flexible( + child: SpinKitThreeBounce( + size: 32, + color: Theme.of(context).colorScheme.secondary, + )), + Text( + 'این کار ممکن است کمی طول بکشد', + style: AppTextStyles.body5 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ) + ], + ), + ); + } + + Widget error(BuildContext context, Function()? onRetry) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: AppColors.red.defaultShade), + child: Center( + child: Text( + 'خطا لطفا مجددا تلاش کنید', + style: AppTextStyles.body5 + .copyWith(color: Colors.white, fontWeight: FontWeight.bold), + ), + ), + ), + const SizedBox( + height: 8, + ), + // LoadingButton( + // color: AppColors.red.defaultShade, + // onPressed: () { + // context.go(Routes.purchase); + // }, + // child: Text( + // 'افزایش اعتبار', + // style: AppTextStyles.body4.copyWith(color: Colors.white), + // ), + // ) + ], + ); + } +} diff --git a/lib/ui/screens/gmedia/chats/video_chat_page.dart b/lib/ui/screens/gmedia/chats/video_chat_page.dart new file mode 100644 index 0000000..0ae7b26 --- /dev/null +++ b/lib/ui/screens/gmedia/chats/video_chat_page.dart @@ -0,0 +1,969 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:math'; + +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/gen/my_flutter_app_icons.dart'; +import 'package:hoshan/core/services/file_manager/download_file_services.dart'; +import 'package:hoshan/core/services/file_manager/pick_file_services.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/data/model/ai/chats_history_model.dart'; +import 'package:hoshan/data/model/ai/messages_model.dart'; +import 'package:hoshan/data/model/ai/send_message_model.dart'; +import 'package:hoshan/ui/screens/chat/bloc/messages_bloc.dart'; +import 'package:hoshan/ui/screens/library/bloc/chats_history_bloc.dart'; +import 'package:hoshan/ui/screens/gmedia/cubit/media_g_response_cubit.dart'; +import 'package:hoshan/ui/screens/gmedia/effects_screen.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/circle_icon_btn.dart'; +import 'package:hoshan/ui/widgets/components/dialog/dialog_handler.dart'; +import 'package:hoshan/ui/widgets/components/dropdown/hint_tooltip.dart'; +import 'package:hoshan/ui/widgets/components/image/custome_image.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; +import 'package:hoshan/ui/widgets/components/video/chat_video_player.dart'; +import 'package:hoshan/ui/widgets/components/video/primary_controls.dart'; +import 'package:share_plus/share_plus.dart'; + +class VideoChatPage extends StatefulWidget { + final bool textToVideo; + final Bots bot; + final int? chatId; + final double maxWidth; + const VideoChatPage( + {super.key, + required this.textToVideo, + required this.bot, + this.chatId, + required this.maxWidth}); + + @override + State createState() => _TtaPageState(); +} + +class _TtaPageState extends State { + final FocusNode _textFieldFocus = FocusNode(); + final CarouselSliderController _carouselController = + CarouselSliderController(); + final ValueNotifier _currentIndex = ValueNotifier(3); + final TextEditingController _query = TextEditingController(); + final ValueNotifier isGhost = ValueNotifier(false); + final ValueNotifier maxSize = ValueNotifier(1); + + late int? chatId = widget.chatId; + late Bots bot = widget.bot; + late bool textToVideo = widget.textToVideo; + + List> groupMessages(List messages) { + return messages.fold>>([], (acc, message) { + if (acc.isEmpty || + (acc.last.first.fromBot != message.fromBot && acc.last.length == 2) || + (acc.last.first.fromBot == message.fromBot && message.fromBot!) || + (acc.last.first.fromBot == message.fromBot && !message.fromBot!)) { + acc.add([message]); + } else { + acc.last.add(message); + } + return acc; + }); + } + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) { + final messagesBloc = MessagesBloc()..add(ResetMessages()); + if (chatId != null) { + messagesBloc.add(GetallMessages(chatId: chatId!)); + } + return messagesBloc; + }, + ), + ], + child: Scaffold( + floatingActionButton: ValueListenableBuilder( + valueListenable: _currentIndex, + builder: (context, value, _) { + return ValueListenableBuilder( + valueListenable: maxSize, + builder: (context, size, child) { + return value < size - 1 + ? Padding( + padding: const EdgeInsets.only(bottom: 64.0), + child: FloatingActionButton.small( + shape: const CircleBorder(), + onPressed: () { + _carouselController.animateToPage(size - 1); + }, + child: Transform.rotate( + angle: pi / 2, + child: Assets.icon.outline.arrowRight.svg( + color: Colors.white, + ), + ), + )) + : const SizedBox.shrink(); + }, + ); + }), + bottomSheet: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + textToVideo + ? Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Directionality( + textDirection: TextDirection.rtl, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + CircleIconBtn( + icon: Assets.icon.bold.send, + color: + Theme.of(context).colorScheme.primary, + iconColor: Colors.white, + iconPadding: const EdgeInsets.all(6), + size: 26, + onTap: () { + if (context + .read() + .state is MediaGResponseLoading) { + return; + } + context + .read() + .request(SendMessageModel( + messageId: DateTime.now() + .toIso8601String(), + id: chatId, + ghost: isGhost.value, + query: _query.text, + botId: bot.id, + )); + _query.clear(); + }, + ), + const SizedBox( + width: 8, + ), + Expanded( + child: TextField( + controller: _query, + focusNode: _textFieldFocus, + onTapOutside: (event) { + _textFieldFocus.unfocus(); + }, + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + decoration: InputDecoration.collapsed( + hintText: + 'ویدیویی که می‌خوای رو توصیف کن...', + hintStyle: AppTextStyles.body4 + .copyWith( + color: AppColors.gray[context + .read< + ThemeModeCubit>() + .isDark() + ? 600 + : 900])), + ), + ), + ], + ), + ), + ), + ) + : Expanded( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0), + child: EffectsBtn( + onClick: (name) async { + if (context.read().state + is MediaGResponseLoading) { + return; + } + FilePickerResult? result = + await FilePicker.platform.pickFiles( + type: FileType.image, + ); + if (result != null && result.files.isNotEmpty) { + final check = + await PickFileService.isImageValid( + result.xFiles.first); + if (check) { + context.read().request( + SendMessageModel( + ghost: isGhost.value, + option: name, + messageId: DateTime.now() + .toIso8601String(), + file: result.xFiles.first, + botId: bot.id, + id: chatId)); + } else { + SnackBarManager(context).show( + message: + 'حجم فایل آپلود شده بیش از حجم مجاز است. (حداکثر ۴ مگابایت)', + status: SnackBarStatus.error); + } + } + }, + ), + ), + ) + ], + ), + ], + ), + ), + body: Stack( + children: [ + Assets.image.videoBack.image( + width: widget.maxWidth, + height: MediaQuery.sizeOf(context).height, + // color: Theme.of(context).scaffoldBackgroundColor, + // colorBlendMode: BlendMode.multiply, + opacity: AlwaysStoppedAnimation( + context.read().isDark() ? 0.4 : 0.8), + fit: BoxFit.cover, + ), + Positioned.fill(child: BlocBuilder( + builder: (context, mState) { + if (mState is MessagesFail) { + return const SizedBox(); + } + if (mState is MessagesLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + final m = mState.messages; + List> messages = groupMessages(m); + return BlocConsumer( + listener: (context, state) async { + if (state is MediaGResponseLoading) { + await Future.delayed(const Duration(milliseconds: 600)); + _carouselController.animateToPage(maxSize.value); + } + if (state is MediaGResponseFail) { + SnackBarManager(context).show( + message: + 'خطا از طرف سرور لطفا لحظاتی دیگر دوباره تلاش کنید', + status: SnackBarStatus.error); + } + if (state is MediaGResponseSucess) { + context.read().add(AddMessage( + message: Messages( + query: state.query, + file: state.file, + createdAt: DateTime.now().toIso8601String(), + error: state.response.error, + id: state.response.humanMessageId, + role: 'user'))); + if (!(state.response.error ?? true)) { + context.read().add(AddMessage( + message: Messages( + content: [ + Content( + videoUrl: + FileUrl(url: state.response.content)) + ], + createdAt: DateTime.now().toIso8601String(), + error: state.response.error, + id: state.response.aiMessageId, + role: 'ai'))); + } + + if (chatId == null && !isGhost.value) { + context.read().add(AddChat( + chats: Chats( + bot: bot, + title: state.response.chatTitle, + createdAt: DateTime.now().toIso8601String(), + id: state.response.chatId))); + } + + chatId = state.response.chatId; + } + }, + builder: (context, state) { + maxSize.value = messages.length + + 1 + + (state is MediaGResponseLoading ? 1 : 0); + return CarouselSlider.builder( + carouselController: _carouselController, + itemCount: messages.length + + 1 + + (state is MediaGResponseLoading ? 1 : 0), + options: CarouselOptions( + initialPage: 3, + viewportFraction: 1, + enlargeFactor: 0.1, + height: MediaQuery.sizeOf(context).height, + autoPlay: false, + scrollDirection: Axis.vertical, + enableInfiniteScroll: false, + onPageChanged: (index, reason) { + _currentIndex.value = index; + }), + itemBuilder: (context, index, realIndex) { + if (state is MediaGResponseLoading && + index == messages.length + 1) { + final yourScrollController = ScrollController(); + + return Column( + children: [ + Flexible( + flex: 1, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + !textToVideo + ? Container( + constraints: BoxConstraints( + maxWidth: + widget.maxWidth * 0.8, + minWidth: + widget.maxWidth * 0.3, + maxHeight: MediaQuery.sizeOf(context) + .height * + 0.3), + padding: + const EdgeInsets.all(16), + margin: + const EdgeInsets.all(16), + decoration: BoxDecoration( + color: context + .read< + ThemeModeCubit>() + .isDark() + ? AppColors.black[900] + : Colors.white, + borderRadius: + BorderRadius.circular(16) + .copyWith( + bottomLeft: + Radius.zero)), + child: AspectRatio( + aspectRatio: 3 / 4, + child: ClipRRect( + borderRadius: + BorderRadius.circular( + 12), + child: CustomeImage( + fit: BoxFit.cover, + src: state.file!.path), + ), + )) + : Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.sizeOf( + context) + .width * + 0.8, + minWidth: MediaQuery.sizeOf( + context) + .width * + 0.3, + maxHeight: + MediaQuery.sizeOf( + context) + .height * + 0.3), + padding: + const EdgeInsets.all(16), + margin: + const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .primary, + borderRadius: BorderRadius + .circular(16) + .copyWith( + bottomRight: + Radius.zero)), + child: Directionality( + textDirection: + TextDirection.rtl, + child: textLay( + yourScrollController, + state.query ?? ''), + ), + ), + ], + )), + Flexible( + child: Column( + children: [ + loading(context), + ], + )) + ], + ); + } else if (index != 0) { + final ms = messages[index - 1]; + Messages? user; + Messages? ai; + if (ms.length == 2) { + user = ms.first; + ai = ms.last; + } else if (ms.length == 1) { + if (ms.single.fromBot ?? false) { + ai = ms.single; + } else { + user = ms.single; + } + } + final yourScrollController = ScrollController(); + return Column( + children: [ + const SizedBox( + height: 16, + ), + if (user != null) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + !textToVideo + ? Container( + constraints: BoxConstraints( + maxWidth: + widget.maxWidth * 0.8, + minWidth: + widget.maxWidth * 0.3, + maxHeight: + MediaQuery.sizeOf(context) + .height * + 0.3), + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: context + .read< + ThemeModeCubit>() + .isDark() + ? AppColors.black[900] + : Colors.white, + borderRadius: + BorderRadius.circular(16) + .copyWith( + bottomLeft: + Radius.zero)), + child: AspectRatio( + aspectRatio: 3 / 4, + child: user.file != null + ? ClipRRect( + borderRadius: + BorderRadius + .circular(12), + child: CustomeImage( + src: user.file!.path, + fit: BoxFit.cover, + )) + : ImageNetwork( + width: double.infinity, + height: double.infinity, + radius: 12, + showHero: true, + url: user + .content + ?.first + .imageUrl + ?.url ?? + ''), + )) + : Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Container( + width: widget.maxWidth * 0.8, + constraints: BoxConstraints( + maxHeight: + MediaQuery.sizeOf( + context) + .height * + 0.4), + padding: + const EdgeInsets.all(16), + margin: + const EdgeInsets.all(16) + .copyWith(bottom: 8), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .primary, + borderRadius: BorderRadius + .circular(16) + .copyWith( + bottomRight: + Radius.zero)), + child: Directionality( + textDirection: + TextDirection.rtl, + child: textLay( + yourScrollController, + user.query ?? ''), + ), + ), + Padding( + padding: + const EdgeInsets.only( + left: 18.0, + bottom: 16), + child: Row( + mainAxisAlignment: + MainAxisAlignment.start, + children: [ + CircleIconBtn( + color: Theme.of(context) + .colorScheme + .primary, + iconColor: Colors.white, + icon: Assets + .icon.outline.copy, + onTap: () async { + try { + await Clipboard.setData( + ClipboardData( + text: user! + .query!)); + Future.delayed( + Duration.zero, + () => SnackBarManager( + context, + id: + 'Copy') + .show( + message: + 'متن کپی شد 😃')); + } catch (e) { + if (kDebugMode) { + print(e); + } + } + }, + ), + ], + ), + ), + ], + ), + ], + ), + if (user?.error ?? false) + error( + context, + () { + context.read().add( + DeleteMessageWithId( + messageId: user!.id!)); + context + .read() + .request(SendMessageModel( + id: chatId, + query: textToVideo + ? _query.text + : null, + file: + textToVideo ? null : user.file, + botId: bot.id, + ghost: isGhost.value, + messageId: DateTime.now() + .toIso8601String(), + )); + }, + ), + if (ai != null) + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + Container( + width: widget.maxWidth * 0.8, + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.all(16) + .copyWith(bottom: 8), + decoration: BoxDecoration( + color: + context.read().isDark() + ? AppColors.black[900] + : Colors.white, + borderRadius: + BorderRadius.circular(16) + .copyWith( + bottomLeft: + Radius.zero)), + child: GestureDetector( + onTap: () => DialogHandler( + context: context) + .showVideoHero( + url: ai! + .content! + .first + .videoUrl! + .url ?? + ''), + child: ClipRRect( + borderRadius: + BorderRadius.circular( + 12), + child: ChatVideoPlayer( + src: ai.content!.first + .videoUrl!.url!, + custome: + PrimaryControls(), + ), + ))), + Padding( + padding: const EdgeInsets.only( + right: 18.0, bottom: 16), + child: Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + CircleIconBtn( + color: Theme.of(context) + .colorScheme + .primary, + iconColor: Colors.white, + icon: Assets + .icon.outline.download, + onTap: () { + try { + DownloadFileService + .getFile( + url: ai! + .content! + .first + .videoUrl! + .url!) + .then((value) { + SnackBarManager(context).show( + message: + 'فایل با موفقیت در پوشه Downloads نشست.', + status: + SnackBarStatus + .success); + }); + } catch (e) { + if (kDebugMode) { + print(e); + } + } + }, + ), + const SizedBox( + width: 8, + ), + CircleIconBtn( + color: Theme.of(context) + .colorScheme + .primary, + iconColor: Colors.white, + icon: + Assets.icon.outline.share, + onTap: () async { + try { + await Share.share(ai! + .content! + .first + .videoUrl! + .url + .toString()); + } catch (e) { + if (kDebugMode) { + print( + 'Error in share Text: $e'); + } + } + }, + ), + ], + ), + ) + ], + ), + ], + ), + ], + ); + } + return Stack( + children: [ + Column( + children: [ + const SizedBox( + height: 16, + ), + if (bot.description != null) + Container( + margin: const EdgeInsets.symmetric( + horizontal: 16), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(16), + color: Theme.of(context) + .colorScheme + .surface), + child: Text( + bot.description!, + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + textDirection: TextDirection.rtl, + textAlign: TextAlign.justify, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + ValueListenableBuilder( + valueListenable: isGhost, + builder: (context, g, _) { + return Transform.scale( + scale: 0.8, + child: Switch.adaptive( + value: g, + thumbIcon: + WidgetStateProperty + .resolveWith< + Icon?>( + (Set + states) { + if (states.contains( + WidgetState + .selected)) { + return Icon( + CustomIcons + .ghost, + color: Theme.of( + context) + .colorScheme + .onSurface); + } + return Icon( + Icons.close, + color: Theme.of( + context) + .colorScheme + .onSurface); + }, + ), + onChanged: (value) { + isGhost.value = value; + }, + ), + ); + }), + const SizedBox( + width: 8, + ), + Text( + 'حالت ناشناس', + style: AppTextStyles.body4 + .copyWith( + color: Theme.of(context) + .colorScheme + .onSurface, + fontWeight: + FontWeight.bold), + ), + const SizedBox( + width: 8, + ), + Padding( + padding: const EdgeInsets.only( + bottom: 4.0), + child: HintTooltip( + hint: + 'با فعال کردن این گزینه؛ چت‌های شما در قسمت تاریخچه، ذخیره نمی‌شوند و اطلاعاتتان ناشناس باقی می‌ماند.', + iconColor: Theme.of(context) + .colorScheme + .onSurface, + ), + ) + ], + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .secondary, + borderRadius: + BorderRadius.circular(12)), + child: Row( + children: [ + Text( + bot.cost == 0 || + bot.cost == null + ? 'رایگان' + : bot.cost.toString(), + style: AppTextStyles.body3 + .copyWith( + color: Colors.white), + ), + const SizedBox( + width: 4, + ), + Assets.icon.outline.coin.svg( + color: Colors.white, + width: 18, + height: 18) + ], + ), + ), + ], + ), + ), + const SizedBox( + height: 32, + ), + ], + ), + // Positioned( + // left: 16, + // bottom: 74, + // child: CircleIconBtn( + // onTap: () { + // DialogHandler(context: context) + // .onVideoCreate(); + // }, + // size: 32, + // icon: Assets.icon.outline.idea)) + ], + ); + }); + }, + ); + }, + )) + ], + ), + ), + ); + } + + Scrollbar textLay(ScrollController yourScrollController, String text) { + return Scrollbar( + thumbVisibility: true, + trackVisibility: true, + interactive: true, + controller: yourScrollController, + radius: const Radius.circular(16), + child: SingleChildScrollView( + controller: yourScrollController, + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: SelectableText( + text, + + style: AppTextStyles.body4.copyWith( + color: Colors.white, + ), + // overflow: TextOverflow.ellipsis, + // textAlign: TextAlign.justify, + ), + ), + ), + ); + } + + Widget error(BuildContext context, Function()? onRetry) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: AppColors.red.defaultShade), + child: Center( + child: Text( + 'خطا لطفا مجددا تلاش کنید ', + style: AppTextStyles.body5 + .copyWith(color: Colors.white, fontWeight: FontWeight.bold), + ), + ), + ), + const SizedBox( + height: 8, + ), + // LoadingButton( + // color: AppColors.red.defaultShade, + // onPressed: () { + // context.go(Routes.purchase); + // }, + // child: Text( + // 'افزایش اعتبار', + // style: AppTextStyles.body4.copyWith(color: Colors.white), + // ), + // ) + ], + ); + } + + Container loading(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: context.read().isDark() + ? AppColors.black[900] + : AppColors.secondryColor[50]), + child: Row( + children: [ + Expanded( + child: SpinKitThreeBounce( + size: 32, + color: Theme.of(context).colorScheme.secondary, + )), + Text( + 'این کار ممکن است کمی طول بکشد', + style: AppTextStyles.body5 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ) + ], + ), + ); + } +} diff --git a/lib/ui/screens/gmedia/cubit/effects_cubit.dart b/lib/ui/screens/gmedia/cubit/effects_cubit.dart new file mode 100644 index 0000000..ab1bc39 --- /dev/null +++ b/lib/ui/screens/gmedia/cubit/effects_cubit.dart @@ -0,0 +1,25 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/effects_model.dart'; +import 'package:hoshan/data/repository/bot_repository.dart'; + +part 'effects_state.dart'; + +class EffectsCubit extends Cubit { + EffectsCubit() : super(EffectsInitial()); + + void getAllEffects() async { + emit(EffectsLoading()); + try { + final response = await BotRepository.getAllEffects(); + emit(EffectsSuccess(effectsModel: response)); + } on DioException catch (e) { + emit(EffectsFail()); + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } +} diff --git a/lib/ui/screens/gmedia/cubit/effects_state.dart b/lib/ui/screens/gmedia/cubit/effects_state.dart new file mode 100644 index 0000000..e9ceeaf --- /dev/null +++ b/lib/ui/screens/gmedia/cubit/effects_state.dart @@ -0,0 +1,20 @@ +part of 'effects_cubit.dart'; + +sealed class EffectsState extends Equatable { + const EffectsState(); + + @override + List get props => []; +} + +final class EffectsInitial extends EffectsState {} + +final class EffectsLoading extends EffectsState {} + +final class EffectsSuccess extends EffectsState { + final EffectsModel effectsModel; + + const EffectsSuccess({required this.effectsModel}); +} + +final class EffectsFail extends EffectsState {} diff --git a/lib/ui/screens/gmedia/cubit/media_g_response_cubit.dart b/lib/ui/screens/gmedia/cubit/media_g_response_cubit.dart new file mode 100644 index 0000000..7d2c5ca --- /dev/null +++ b/lib/ui/screens/gmedia/cubit/media_g_response_cubit.dart @@ -0,0 +1,59 @@ +import 'dart:convert'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/ai/ai_response_model.dart'; +import 'package:hoshan/data/model/ai/send_message_model.dart'; +import 'package:hoshan/data/repository/chatbot_repository.dart'; +import 'package:image_picker/image_picker.dart'; + +part 'media_g_response_state.dart'; + +class MediaGResponseCubit extends Cubit { + MediaGResponseCubit() : super(MediaGResponseInitial()); + + void request(SendMessageModel model) async { + emit(MediaGResponseLoading(file: model.file, query: model.query)); + try { + AiResponseModel response = AiResponseModel(); + await for (String message in ChatbotRepository.sendMessageTool(model)) { + late Map jsonMap; + try { + jsonMap = jsonDecode(message); + } catch (e) { + if (kDebugMode) { + print('Error in Parse: $e'); + } + } + + final r = AiResponseModel.fromJson(jsonMap) + .copyWith(humanMessageId: model.messageId); + response = response.copyWith( + aiMessageId: r.aiMessageId, + chatId: r.chatId, + chatTitle: r.chatTitle, + content: r.content, + credit: r.credit, + detail: r.detail, + error: r.content?.startsWith('Check failed') ?? r.error, + freeCredit: r.freeCredit, + humanMessageId: r.humanMessageId, + statusCode: r.statusCode); + } + + emit(MediaGResponseSucess( + response: response, + file: model.file, + query: model.query, + error: response.error ?? false + ? 'مشکلی از طرف سرور رخ داده است' + : null)); + } catch (e) { + if (kDebugMode) { + print('Dio Error is: $e'); + } + emit(MediaGResponseFail()); + } + } +} diff --git a/lib/ui/screens/gmedia/cubit/media_g_response_state.dart b/lib/ui/screens/gmedia/cubit/media_g_response_state.dart new file mode 100644 index 0000000..6773499 --- /dev/null +++ b/lib/ui/screens/gmedia/cubit/media_g_response_state.dart @@ -0,0 +1,29 @@ +part of 'media_g_response_cubit.dart'; + +sealed class MediaGResponseState extends Equatable { + const MediaGResponseState(); + + @override + List get props => []; +} + +final class MediaGResponseInitial extends MediaGResponseState {} + +final class MediaGResponseLoading extends MediaGResponseState { + final XFile? file; + final String? query; + + const MediaGResponseLoading({this.file, this.query}); +} + +final class MediaGResponseSucess extends MediaGResponseState { + final AiResponseModel response; + final XFile? file; + final String? query; + final String? error; + + const MediaGResponseSucess( + {required this.response, this.file, this.query, this.error}); +} + +final class MediaGResponseFail extends MediaGResponseState {} diff --git a/lib/ui/screens/gmedia/cubit/medias_cubit.dart b/lib/ui/screens/gmedia/cubit/medias_cubit.dart new file mode 100644 index 0000000..96440db --- /dev/null +++ b/lib/ui/screens/gmedia/cubit/medias_cubit.dart @@ -0,0 +1,25 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/media_model.dart'; +import 'package:hoshan/data/repository/bot_repository.dart'; + +part 'medias_state.dart'; + +class MediasCubit extends Cubit { + MediasCubit() : super(MediasInitial()); + + void getMedias() async { + emit(MediasLoading()); + try { + final response = await BotRepository.getMedias(); + emit(MediasSuccess(medias: response)); + } on DioException catch (e) { + emit(MediasFail()); + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } +} diff --git a/lib/ui/screens/gmedia/cubit/medias_state.dart b/lib/ui/screens/gmedia/cubit/medias_state.dart new file mode 100644 index 0000000..cd9931f --- /dev/null +++ b/lib/ui/screens/gmedia/cubit/medias_state.dart @@ -0,0 +1,20 @@ +part of 'medias_cubit.dart'; + +sealed class MediasState extends Equatable { + const MediasState(); + + @override + List get props => []; +} + +final class MediasInitial extends MediasState {} + +final class MediasLoading extends MediasState {} + +final class MediasSuccess extends MediasState { + final MediasModel medias; + + const MediasSuccess({required this.medias}); +} + +final class MediasFail extends MediasState {} diff --git a/lib/ui/screens/gmedia/cubit/single_media_cubit.dart b/lib/ui/screens/gmedia/cubit/single_media_cubit.dart new file mode 100644 index 0000000..b2f588d --- /dev/null +++ b/lib/ui/screens/gmedia/cubit/single_media_cubit.dart @@ -0,0 +1,25 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/photo_gen_model.dart'; +import 'package:hoshan/data/repository/bot_repository.dart'; + +part 'single_media_state.dart'; + +class SingleMediaCubit extends Cubit { + SingleMediaCubit() : super(SingleMediaInitial()); + + void getMediaById(int id) async { + emit(SingleMediaLoading()); + try { + final response = await BotRepository.getSingleMedia(id); + emit(SingleMediaSuccess(medias: response)); + } on DioException catch (e) { + emit(SingleMediaFail()); + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } +} diff --git a/lib/ui/screens/gmedia/cubit/single_media_state.dart b/lib/ui/screens/gmedia/cubit/single_media_state.dart new file mode 100644 index 0000000..f03edad --- /dev/null +++ b/lib/ui/screens/gmedia/cubit/single_media_state.dart @@ -0,0 +1,20 @@ +part of 'single_media_cubit.dart'; + +sealed class SingleMediaState extends Equatable { + const SingleMediaState(); + + @override + List get props => []; +} + +final class SingleMediaInitial extends SingleMediaState {} + +final class SingleMediaLoading extends SingleMediaState {} + +final class SingleMediaSuccess extends SingleMediaState { + final GensModel medias; + + const SingleMediaSuccess({required this.medias}); +} + +final class SingleMediaFail extends SingleMediaState {} diff --git a/lib/ui/screens/gmedia/effects_screen.dart b/lib/ui/screens/gmedia/effects_screen.dart new file mode 100644 index 0000000..5307dc1 --- /dev/null +++ b/lib/ui/screens/gmedia/effects_screen.dart @@ -0,0 +1,154 @@ +// ignore_for_file: deprecated_member_use + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/ui/screens/gmedia/cubit/effects_cubit.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:popover/popover.dart'; + +class EffectsBtn extends StatelessWidget { + final Function(String name)? onClick; + const EffectsBtn({super.key, this.onClick}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: LoadingButton( + width: double.infinity, + onPressed: () async { + showPopover( + context: context, + bodyBuilder: (context) => EffectsScreen( + onClick: onClick, + ), + direction: PopoverDirection.bottom, + // constraints: const BoxConstraints( + // minWidth: 200, + // maxWidth: 600, + // maxHeight: 600, + // minHeight: 200), + + arrowHeight: 15, + arrowWidth: 30, + radius: 16, + barrierDismissible: true, + + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + ); + }, + backgroundColor: Theme.of(context).colorScheme.primary, + child: Text( + 'بارگذاری عکس', + style: AppTextStyles.body4.copyWith(color: Colors.white), + )), + ); + } +} + +class EffectsScreen extends StatefulWidget { + final Function(String name)? onClick; + const EffectsScreen({super.key, this.onClick}); + + @override + State createState() => _EffectsScreenState(); +} + +class _EffectsScreenState extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => EffectsCubit()..getAllEffects(), + child: Container( + width: MediaQuery.sizeOf(context).width * 0.8, + height: MediaQuery.sizeOf(context).height * 0.5, + padding: const EdgeInsets.all(16), + child: Directionality( + textDirection: TextDirection.rtl, + child: Column( + children: [ + ListTile( + title: Text( + '✨ افکت‌ها', + style: AppTextStyles.body3 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ), + ), + Expanded( + child: Scrollbar( + thumbVisibility: true, + trackVisibility: true, + interactive: true, + child: BlocBuilder( + builder: (context, state) { + if (state is EffectsFail) { + return const SizedBox.shrink(); + } + if (state is EffectsSuccess) { + return ListView.builder( + itemCount: state.effectsModel.effects?.length, + physics: const BouncingScrollPhysics(), + shrinkWrap: true, + padding: const EdgeInsets.only(left: 16), + itemBuilder: (context, index) { + final effect = state.effectsModel.effects![index]; + return Padding( + padding: + const EdgeInsets.symmetric(vertical: 8.0), + child: InkWell( + onTap: () { + widget.onClick?.call(effect.name!); + context.pop(); + }, + child: Stack( + children: [ + AspectRatio( + aspectRatio: 16 / 9, + child: ImageNetwork( + radius: 16, url: effect.gif ?? ''), + ), + Positioned.fill( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.black.withOpacity(0.5), + Colors.transparent + ], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + ), + borderRadius: + BorderRadius.circular(16), + ), + alignment: Alignment.bottomRight, + child: Text( + effect.name ?? '', + style: AppTextStyles.body4 + .copyWith(color: Colors.white), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + return const Center(child: CircularProgressIndicator()); + }, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/ui/screens/gmedia/generators/generate_audio_page.dart b/lib/ui/screens/gmedia/generators/generate_audio_page.dart new file mode 100644 index 0000000..ef594e0 --- /dev/null +++ b/lib/ui/screens/gmedia/generators/generate_audio_page.dart @@ -0,0 +1,398 @@ +// ignore_for_file: deprecated_member_use + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/data/storage/shared_preferences_helper.dart'; +import 'package:hoshan/ui/screens/library/bloc/chats_history_bloc.dart'; +import 'package:hoshan/ui/screens/library/library_screen.dart'; +import 'package:hoshan/ui/screens/gmedia/cubit/media_g_response_cubit.dart'; +import 'package:hoshan/ui/screens/gmedia/cubit/single_media_cubit.dart'; +import 'package:hoshan/ui/screens/gmedia/chats/audio_chat_page.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/dialog/dialog_handler.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; + +class GenerateAudioPage extends StatefulWidget { + final int? id; + const GenerateAudioPage({super.key, this.id}); + + @override + State createState() => _GenerateAudioPageState(); +} + +class _GenerateAudioPageState extends State + with TickerProviderStateMixin { + Bots? selectedBot; + int? chatId; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().add(RestartChatsHistory()); + context.read().add(const GetAllChats(type: 'audio')); + + if (!GuidsStorage.isSeenAudio()) { + DialogHandler(context: context).onMusicCreate(); + GuidsStorage.setSeenAudio(true); + } + + if (widget.id != null) { + context.read().getMediaById(widget.id!); + } + }); + } + + @override + void dispose() { + tabController.dispose(); + super.dispose(); + } + + late final TabController tabController = + TabController(length: 3, vsync: this); + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + if (state is SingleMediaFail) { + context.pop(); + SnackBarManager(context).show( + message: + 'خطا در برقراری ارتباط با سرور لطفا لحظاتی دیگر امتحان کنید', + status: SnackBarStatus.error); + } + }, + builder: (context, state) { + if (state is SingleMediaFail) { + return const SizedBox.shrink(); + } + if (state is SingleMediaSuccess) { + return SizedBox( + child: DefaultTabController( + length: state.medias.categories?.length ?? 0, + child: Scaffold( + body: Responsive(context).maxWidthInDesktop( + child: (contxet, mw) => Scaffold( + drawer: Drawer( + shape: const BeveledRectangleBorder( + borderRadius: BorderRadius.zero), + child: LibraryScreen( + type: 'audio', + onTap: (chat) { + final id = chat.bot?.category?.id; + if (id != null) { + final index = state.medias.categories! + .indexOf(state.medias.categories!.firstWhere( + (element) { + return element.id == id; + }, + )); + + tabController.animateTo(index); + Navigator.of(context).pop(); // Close the drawer + setState(() { + chatId = chat.id; + selectedBot = chat.bot; + }); + } + }, + ), + ), + appBar: AppBar( + toolbarHeight: 28, + backgroundColor: context.read().isDark() + ? null + : AppColors.primaryColor[50], + bottom: TabBar( + controller: tabController, + dividerColor: Colors.transparent, + onTap: (value) { + setState(() { + selectedBot = null; + chatId = null; + }); + }, + tabs: List.generate( + state.medias.categories?.length ?? 0, + (index) { + return Tab( + height: 80, + child: AnimatedBuilder( + animation: tabController, + builder: (context, _) { + return Column( + children: [ + if (state.medias.categories?[index] + .icon != + null) + Expanded( + child: SvgPicture.network( + DioService.baseURL + + state.medias + .categories![index].icon!, + color: tabController.index == + index + ? Theme.of(context) + .colorScheme + .primary + : AppColors.gray[context + .read< + ThemeModeCubit>() + .isDark() + ? 400 + : 800], + ), + ), + Text( + state.medias.categories?[index] + .name ?? + '', + style: AppTextStyles.body4.copyWith( + fontWeight: + tabController.index == index + ? FontWeight.bold + : FontWeight.normal, + color: tabController.index == + index + ? Theme.of(context) + .colorScheme + .primary + : AppColors.gray[context + .read< + ThemeModeCubit>() + .isDark() + ? 400 + : 800]), + ), + const SizedBox( + height: 8, + ), + ], + ); + }), + ); + }, + ), + ), + ), + body: TabBarView( + key: ValueKey(chatId), // Change key to force rebuild + physics: const NeverScrollableScrollPhysics(), + controller: tabController, + children: List.generate( + state.medias.categories!.length, + (index) { + switch (index) { + case 0: + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) { + return MediaGResponseCubit(); + }, + ), + ], + child: AudioChatPage( + type: 'text', + chatId: chatId, + bot: selectedBot ?? + state.medias.categories![index].bots! + .first, + maxWidth: mw, + ), + ); + case 1: + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) { + return MediaGResponseCubit(); + }, + ), + ], + child: AudioChatPage( + type: 'file', + chatId: chatId, + bot: selectedBot ?? + state.medias.categories![index].bots! + .first, + maxWidth: mw, + ), + ); + case 2: + if (selectedBot != null) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) { + return MediaGResponseCubit(); + }, + ), + ], + child: AudioChatPage( + type: 'music', + chatId: chatId, + bot: selectedBot!, + maxWidth: mw, + ), + ); + } + final category = state.medias.categories![index]; + return MasonryGridView.builder( + itemCount: category.bots?.length, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric( + vertical: 16, horizontal: 16), + gridDelegate: + const SliverSimpleGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2), + itemBuilder: (context, index) { + final bot = category.bots?[index]; + return Padding( + // color: Colors.amberAccent, + padding: const EdgeInsets.all(8), + + child: InkWell( + onTap: () { + setState(() { + selectedBot = bot; + }); + }, + child: Container( + constraints: BoxConstraints( + maxHeight: + MediaQuery.sizeOf(context) + .height * + 0.3), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .surface, + borderRadius: + BorderRadius.circular(16)), + child: Stack( + children: [ + ConstrainedBox( + constraints: BoxConstraints( + minHeight: mw * 0.3, + minWidth: mw * 0.3), + child: ImageNetwork( + radius: 16, + url: bot?.image ?? '', + fit: BoxFit.cover, + ), + ), + Positioned.fill( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(16), + gradient: LinearGradient( + begin: + Alignment.topCenter, + end: Alignment + .bottomCenter, + colors: [ + Theme.of(context) + .colorScheme + .surface + .withAlpha(40), + AppColors + .primaryColor[600] + .withAlpha(180) + ])), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.end, + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + Text( + bot?.name ?? '', + style: AppTextStyles.body4 + .copyWith( + color: Colors.white, + fontWeight: + FontWeight + .bold), + textDirection: + TextDirection.rtl, + maxLines: 2, + overflow: + TextOverflow.ellipsis, + ), + Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + Padding( + padding: + const EdgeInsets + .only(top: 4.0), + child: Text( + bot?.cost + ?.toString() ?? + '', + style: AppTextStyles + .body4 + .copyWith( + color: Colors + .white), + ), + ), + const SizedBox( + width: 4, + ), + Assets.icon.outline.coin + .svg( + color: + Colors.white, + width: 18, + height: 18) + ], + ) + ], + ), + )) + ], + ), + ), + ), + ); + }, + ); + default: + return const SizedBox.shrink(); + } + }, + ), + ), + ), + ), + ), + ), + ); + } + return const Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + }, + ); + } +} diff --git a/lib/ui/screens/gmedia/generators/generate_photo_page.dart b/lib/ui/screens/gmedia/generators/generate_photo_page.dart new file mode 100644 index 0000000..57983ef --- /dev/null +++ b/lib/ui/screens/gmedia/generators/generate_photo_page.dart @@ -0,0 +1,402 @@ +// ignore_for_file: deprecated_member_use + +import 'package:before_after/before_after.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/data/model/chat_args.dart'; +import 'package:hoshan/ui/screens/gmedia/cubit/single_media_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; + +class GeneratePhotoPage extends StatefulWidget { + const GeneratePhotoPage({super.key}); + + @override + State createState() => _GeneratePhotoPageState(); +} + +class _GeneratePhotoPageState extends State + with TickerProviderStateMixin { + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + if (state is SingleMediaFail) { + context.pop(); + SnackBarManager(context).show( + message: + 'خطا در برقراری ارتباط با سرور لطفا لحظاتی دیگر امتحان کنید', + status: SnackBarStatus.error); + } + }, + builder: (context, state) { + if (state is SingleMediaFail) { + return const SizedBox.shrink(); + } + if (state is SingleMediaSuccess) { + final TabController tabController = TabController( + length: state.medias.categories?.length ?? 0, vsync: this); + + return DefaultTabController( + length: state.medias.categories?.length ?? 0, + child: Scaffold( + body: Responsive(context).maxWidthInDesktop( + child: (contxet, mw) => Scaffold( + appBar: AppBar( + toolbarHeight: 0, + backgroundColor: context.read().isDark() + ? null + : AppColors.primaryColor[50], + bottom: TabBar( + controller: tabController, + dividerColor: Colors.transparent, + tabs: List.generate( + state.medias.categories?.length ?? 0, + (index) { + return Tab( + height: 80, + child: AnimatedBuilder( + animation: tabController, + builder: (context, _) { + return Column( + children: [ + if (state + .medias.categories?[index].icon != + null) + Expanded( + child: SvgPicture.network( + DioService.baseURL + + state.medias.categories![index] + .icon!, + color: tabController.index == index + ? Theme.of(context) + .colorScheme + .primary + : AppColors.gray[context + .read() + .isDark() + ? 400 + : 800], + ), + ), + Text( + state.medias.categories?[index].name ?? + '', + style: AppTextStyles.body4.copyWith( + fontWeight: + tabController.index == index + ? FontWeight.bold + : FontWeight.normal, + color: tabController.index == index + ? Theme.of(context) + .colorScheme + .primary + : AppColors.gray[context + .read() + .isDark() + ? 400 + : 800]), + ), + const SizedBox( + height: 8, + ), + ], + ); + }), + ); + }, + ), + ), + ), + body: TabBarView( + controller: tabController, + physics: const NeverScrollableScrollPhysics(), + children: List.generate( + state.medias.categories!.length, + (index) { + switch (index) { + case 1: + final category = state.medias.categories![index]; + return MasonryGridView.builder( + itemCount: category.bots?.length, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric( + vertical: 16, horizontal: 16), + gridDelegate: + const SliverSimpleGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2), + itemBuilder: (context, index) { + final bot = category.bots?[index]; + ValueNotifier changeAB = + ValueNotifier(0.5); + + return Padding( + // color: Colors.amberAccent, + padding: const EdgeInsets.all(8), + child: Column( + children: [ + Stack( + children: [ + AspectRatio( + aspectRatio: 3 / 4, + child: SizedBox( + height: MediaQuery.sizeOf(context) + .height * + 0.3, + child: Container( + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular( + 16), + color: Theme.of(context) + .colorScheme + .surface), + child: ValueListenableBuilder( + valueListenable: changeAB, + builder: (context, val, _) { + return BeforeAfter( + value: val, + onValueChanged: + (value) { + changeAB.value = + value; + }, + trackWidth: 3, + thumbWidth: 18, + after: ImageNetwork( + radius: 16, + url: + bot?.image2 ?? '', + // url: a, + fit: BoxFit.cover, + width: + double.infinity, + height: + double.infinity, + ), + before: ImageNetwork( + radius: 16, + url: bot?.image ?? '', + // url: b, + + fit: BoxFit.cover, + width: + double.infinity, + height: + double.infinity, + ), + ); + }), + ), + ), + ), + Positioned( + right: 8, + bottom: 12, + child: Container( + padding: + const EdgeInsets.symmetric( + vertical: 4, + horizontal: 8), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .secondary + .withAlpha(180), + borderRadius: + BorderRadius.circular(8)), + child: Row( + children: [ + Padding( + padding: + const EdgeInsets.only( + top: 2.0), + child: Text( + bot!.cost == 0 || + bot.cost == null + ? 'رایگان' + : '${bot.cost}', + style: AppTextStyles.body6 + .copyWith( + color: + Colors.white), + ), + ), + Assets.icon.outline.coin.svg( + color: Colors.white, + width: 16, + height: 16), + ], + ), + ), + ) + ], + ), + const SizedBox( + height: 4, + ), + LoadingButton( + onPressed: () { + context.go(Routes.photoToPhoto, + extra: ChatArgs( + bot: bot, + )); + }, + width: double.infinity, + child: Text( + bot.name ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTextStyles.body4 + .copyWith(color: Colors.white), + )) + ], + ), + ); + }, + ); + case 0: + final category = state.medias.categories![index]; + return MasonryGridView.builder( + itemCount: category.bots?.length, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric( + vertical: 16, horizontal: 16), + gridDelegate: + const SliverSimpleGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2), + itemBuilder: (context, index) { + final bot = category.bots?[index]; + return Padding( + // color: Colors.amberAccent, + padding: const EdgeInsets.all(8), + + child: InkWell( + onTap: () { + context.go(Routes.photoToPhoto, + extra: ChatArgs(bot: bot!)); + }, + child: Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.sizeOf(context) + .height * + 0.3), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .surface, + borderRadius: + BorderRadius.circular(16)), + child: Stack( + children: [ + ImageNetwork( + radius: 16, + url: bot?.image ?? '', + fit: BoxFit.cover, + ), + Positioned.fill( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context) + .colorScheme + .surface + .withAlpha(40), + AppColors + .primaryColor[600] + .withAlpha(180) + ])), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.end, + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + Text( + bot?.name ?? '', + style: AppTextStyles.body4 + .copyWith( + color: Colors.white, + fontWeight: + FontWeight.bold), + textDirection: + TextDirection.rtl, + maxLines: 2, + overflow: + TextOverflow.ellipsis, + ), + Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + Padding( + padding: + const EdgeInsets.only( + top: 4.0), + child: Text( + bot?.cost?.toString() ?? + '', + style: AppTextStyles + .body4 + .copyWith( + color: Colors + .white), + ), + ), + const SizedBox( + width: 4, + ), + Assets.icon.outline.coin + .svg( + color: Colors.white, + width: 18, + height: 18) + ], + ) + ], + ), + )) + ], + ), + ), + ), + ); + }, + ); + default: + return const SizedBox.shrink(); + } + }, + ), + ), + ), + ), + ), + ); + } + return const Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + }, + ); + } +} diff --git a/lib/ui/screens/gmedia/generators/generate_video_page.dart b/lib/ui/screens/gmedia/generators/generate_video_page.dart new file mode 100644 index 0000000..a122b2c --- /dev/null +++ b/lib/ui/screens/gmedia/generators/generate_video_page.dart @@ -0,0 +1,222 @@ +// ignore_for_file: deprecated_member_use + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/data/storage/shared_preferences_helper.dart'; +import 'package:hoshan/ui/screens/library/bloc/chats_history_bloc.dart'; +import 'package:hoshan/ui/screens/library/library_screen.dart'; +import 'package:hoshan/ui/screens/gmedia/cubit/media_g_response_cubit.dart'; +import 'package:hoshan/ui/screens/gmedia/cubit/single_media_cubit.dart'; +import 'package:hoshan/ui/screens/gmedia/chats/video_chat_page.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/dialog/dialog_handler.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; + +class GenerateVideoPage extends StatefulWidget { + final int? id; + const GenerateVideoPage({super.key, this.id}); + + @override + State createState() => _GenerateAudioPageState(); +} + +class _GenerateAudioPageState extends State + with TickerProviderStateMixin { + Bots? selectedBot; + int? chatId; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().add(RestartChatsHistory()); + context.read().add(const GetAllChats(type: 'video')); + + if (!GuidsStorage.isSeenVideo()) { + DialogHandler(context: context).onVideoCreate(); + GuidsStorage.setSeenVideo(true); + } + + if (widget.id != null) { + context.read().getMediaById(widget.id!); + } + }); + } + + @override + void dispose() { + tabController.dispose(); + super.dispose(); + } + + late final TabController tabController = + TabController(length: 3, vsync: this); + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + if (state is SingleMediaFail) { + context.pop(); + SnackBarManager(context).show( + message: + 'خطا در برقراری ارتباط با سرور لطفا لحظاتی دیگر امتحان کنید', + status: SnackBarStatus.error); + } + }, + builder: (context, state) { + if (state is SingleMediaFail) { + return const SizedBox.shrink(); + } + if (state is SingleMediaSuccess) { + return DefaultTabController( + length: state.medias.categories?.length ?? 0, + child: Scaffold( + body: Responsive(context).maxWidthInDesktop( + child: (contxet, mw) => Scaffold( + drawer: Drawer( + shape: const BeveledRectangleBorder( + borderRadius: BorderRadius.zero), + child: LibraryScreen( + type: 'video', + onTap: (chat) { + final id = chat.bot?.category?.id; + if (id != null) { + final index = state.medias.categories! + .indexOf(state.medias.categories!.firstWhere( + (element) { + return element.id == id; + }, + )); + + tabController.animateTo(index); + Navigator.of(context).pop(); // Close the drawer + setState(() { + chatId = chat.id; + selectedBot = chat.bot; + }); + } + }, + ), + ), + appBar: AppBar( + toolbarHeight: 28, + backgroundColor: context.read().isDark() + ? null + : AppColors.primaryColor[50], + bottom: TabBar( + controller: tabController, + dividerColor: Colors.transparent, + onTap: (value) { + setState(() { + selectedBot = null; + chatId = null; + }); + }, + tabs: List.generate( + state.medias.categories?.length ?? 0, + (index) { + return Tab( + height: 80, + child: AnimatedBuilder( + animation: tabController, + builder: (context, _) { + return Column( + children: [ + if (state + .medias.categories?[index].icon != + null) + Expanded( + child: SvgPicture.network( + DioService.baseURL + + state.medias.categories![index] + .icon!, + color: tabController.index == index + ? Theme.of(context) + .colorScheme + .primary + : AppColors.gray[context + .read() + .isDark() + ? 400 + : 800], + ), + ), + Text( + state.medias.categories?[index].name ?? + '', + style: AppTextStyles.body4.copyWith( + fontWeight: + tabController.index == index + ? FontWeight.bold + : FontWeight.normal, + color: tabController.index == index + ? Theme.of(context) + .colorScheme + .primary + : AppColors.gray[context + .read() + .isDark() + ? 400 + : 800]), + ), + const SizedBox( + height: 8, + ), + ], + ); + }), + ); + }, + ), + ), + ), + body: TabBarView( + key: ValueKey(chatId), // Change key to force rebuild + physics: const NeverScrollableScrollPhysics(), + controller: tabController, + children: List.generate( + state.medias.categories!.length, + (index) { + final bot = selectedBot ?? + state.medias.categories![index].bots!.first; + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) { + return MediaGResponseCubit(); + }, + ), + ], + child: VideoChatPage( + textToVideo: bot.attachmentType == null, + chatId: chatId, + bot: bot, + maxWidth: mw, + ), + ); + }, + ), + ), + ), + ), + ), + ); + } + return const Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + }, + ); + } +} diff --git a/lib/ui/screens/gmedia/send_image_modal.dart b/lib/ui/screens/gmedia/send_image_modal.dart new file mode 100644 index 0000000..1ea75f6 --- /dev/null +++ b/lib/ui/screens/gmedia/send_image_modal.dart @@ -0,0 +1,174 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/services/file_manager/pick_file_services.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; + +class SendImageModal extends ModalRoute { + final Function(PlatformFile image) onFileSelected; + + SendImageModal({required this.onFileSelected}); + + @override + Duration get transitionDuration => const Duration(milliseconds: 500); + + @override + bool get opaque => false; + + @override + bool get barrierDismissible => true; + + @override + Color get barrierColor => Colors.black.withAlpha(210); + + @override + String get barrierLabel => 'Images'; + + @override + bool get maintainState => true; + + @override + Widget buildTransitions(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) { + // You can add your own animations for the overlay content + return FadeTransition( + opacity: animation, + child: ScaleTransition( + scale: animation, + child: child, + ), + ); + } + + @override + Widget buildPage(BuildContext context, Animation animation, + Animation secondaryAnimation) { + return Material( + type: MaterialType.transparency, + child: SafeArea( + child: Directionality( + textDirection: TextDirection.rtl, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ListTile( + title: Text( + 'انتخاب تصویر', + style: AppTextStyles.headline6 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ), + ), + Container( + width: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Theme.of(context).colorScheme.onSurface)), + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.photo_library_rounded, + size: 78, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox( + height: 8, + ), + LoadingButton( + backgroundColor: Theme.of(context).colorScheme.primary, + onPressed: () async { + FilePickerResult? result = + await FilePicker.platform.pickFiles( + type: FileType.image, + ); + + if (result != null && result.files.isNotEmpty) { + final check = await PickFileService.isImageValid( + result.xFiles.first); + if (check) { + onFileSelected(result.files.first); + Navigator.of(context).pop(); + } else { + Navigator.of(context).pop(); + + SnackBarManager(context).show( + message: + 'سایز یا اندازه فایل مورد نظر بیش از اندازه است', + status: SnackBarStatus.error); + } + } + }, + child: Text( + 'انتخاب تصویر', + style: + AppTextStyles.body4.copyWith(color: Colors.white), + ), + ), + ], + ), + ), + Text( + 'فقط فرمت‌های jpg ،png ،jpeg', + style: AppTextStyles.body5.copyWith( + color: AppColors.gray[ + context.read().isDark() ? 900 : 400]), + ), + const SizedBox( + height: 16, + ), + ListTile( + leading: Column( + children: [ + const SizedBox( + height: 2, + ), + Assets.icon.outline.infoCircle.svg( + color: Theme.of(context).colorScheme.onSurface, + width: 22, + height: 22), + ], + ), + title: Text( + 'فرمت‌های قابل قبول و غیر قابل قبول برای هوش مصنوعی', + style: AppTextStyles.body4 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Assets.image.expectedFormat + .image(width: MediaQuery.sizeOf(context).width), + ) + // Wrap( + // children: List.generate( + // 5, + // (index) => const Padding( + // padding: EdgeInsets.all(8.0), + // child: ImageNetwork( + // url: + // 'https://s3-alpha-sig.figma.com/img/3e4e/9825/3c786b8ce2f4b99d58a6e235c97e9705?Expires=1742774400&Key-Pair-Id=APKAQ4GOSFWCW27IBOMQ&Signature=isPXc0erkoZOon3ILDBuJwB7v57nAQg7ezzF0UZ~ga9KVpXa5SLAe5o0k29qH5c93tGYncveGfrf0UF2AIszVAee1MLNZu~ToFiB2tYZ9se8yKxba9XT1Qa-4f~KTLuJ5yOPgZFzQeUQOzzd00VZlu5~J5ZxxhHkrbd3-Wz8dzp7fYV-zorwsInlRphP6cU-4C7WleRz8-YULFEgrU5xYSSqZMH8WaAgcL-Ws32Gcts4LP0sJZo5sx3JgPSmhAH9alt7GByu5U7q3CtGAiFux70LkNg-BRQdwsj1zyQEewxqjI~ZXyBRRLxOxnkzmu3uOd~u6~A1KTb9CoEArluQEA__', + // width: 61, + // height: 61, + // radius: 16, + // ), + // ), + // ), + // ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/ui/screens/library/bloc/chats_history_bloc.dart b/lib/ui/screens/library/bloc/chats_history_bloc.dart new file mode 100644 index 0000000..0cf3f40 --- /dev/null +++ b/lib/ui/screens/library/bloc/chats_history_bloc.dart @@ -0,0 +1,163 @@ +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/core/utils/date_time.dart'; +import 'package:hoshan/data/model/ai/chats_history_model.dart'; +import 'package:hoshan/data/model/ai/chats_indates_model.dart'; +import 'package:hoshan/data/repository/chatbot_repository.dart'; + +part 'chats_history_event.dart'; +part 'chats_history_state.dart'; + +class ChatsHistoryBloc extends Bloc { + static List chatsInDates = []; + static List chats = []; + static int page = 1; + static int? lastPage; + + ChatsHistoryBloc() : super(ChatsHistoryInitial()) { + List onOrganizeChats(List items) { + DateTime now = DateTimeUtils.getNow(); + DateTime today = DateTime(now.year, now.month, now.day); + DateTime yesterday = today.subtract(const Duration(days: 1)); + DateTime sevenDaysAgo = today.subtract(const Duration(days: 7)); + + Map> categorizedItems = { + "امروز": [], + "دیروز": [], + '‌هفته گذشته': [], + 'قدیمی‌ترها': [], + }; + + for (var item in items) { + final d = DateTimeUtils.convertStringIsoToDate(item.createdAt!); + DateTime itemDate = DateTime(d.year, d.month, d.day); + + if (itemDate == today) { + categorizedItems["امروز"]!.add(item); + } else if (itemDate == yesterday) { + categorizedItems["دیروز"]!.add(item); + } else if (itemDate.isAfter(sevenDaysAgo) && + itemDate.isBefore(yesterday)) { + categorizedItems['‌هفته گذشته']!.add(item); + } else { + categorizedItems['قدیمی‌ترها']!.add(item); + } + } + // Print results + chatsInDates.clear(); + categorizedItems.forEach((key, value) { + if (value.isNotEmpty) { + chatsInDates.add(ChatsIndatesModel(title: key, chats: value)); + } + }); + + return chatsInDates; + } + + on((event, emit) async { + if (event is GetAllChats) { + if (page - 1 == lastPage) { + return; + } + if (page == 1) { + chats.clear(); + emit(ChatsHistoryInitial()); + } else { + emit(ChatsHistoryLoading(chatsInDates: chatsInDates)); + } + try { + final response = await ChatbotRepository.getChats( + type: event.type, + page: page, + search: event.search, + date: event.date, + archive: event.archive); + page++; + lastPage = response.lastPage; + + chats.addAll(response.chats!); + onOrganizeChats(chats); + + emit(ChatsHistorySuccess(chatsInDates: chatsInDates)); + } on DioException catch (e) { + emit(ChatsHistoryFail()); + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } + + if (event is AddChat) { + emit(ChatsHistoryInitial()); + + if (chatsInDates.isNotEmpty) { + chatsInDates.first.chats.insert(0, event.chats); + emit(ChatsHistorySuccess(chatsInDates: chatsInDates)); + } else { + chatsInDates + .add(ChatsIndatesModel(title: 'امروز', chats: [event.chats])); + emit(ChatsHistorySuccess(chatsInDates: chatsInDates)); + } + } + + if (event is RemoveChat) { + emit(ChatsHistoryInitial()); + + try { + if (event.withCall) { + await ChatbotRepository.deleteChat(id: event.chats.id!); + } + int? index; + int? mainIndex; + for (var chatInDate in chatsInDates) { + for (var chat in chatInDate.chats) { + if (chat == event.chats) { + index = chatInDate.chats.indexOf(chat); + mainIndex = chatsInDates.indexOf(chatInDate); + } + } + } + if (mainIndex != null && index != null) { + chatsInDates[mainIndex].chats.removeAt(index); + if (chatsInDates[mainIndex].chats.isEmpty) { + chatsInDates.removeAt(mainIndex); + } + } + + emit(ChatsHistorySuccess(chatsInDates: chatsInDates)); + } on DioException catch (e) { + // emit(ChatsHistoryFail()); + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } + + if (event is RemoveAll) { + emit(ChatsHistoryInitial()); + + try { + await ChatbotRepository.deleteAllChats(archive: event.archive); + chatsInDates.clear(); + + emit(ChatsHistorySuccess(chatsInDates: chatsInDates)); + } on DioException catch (e) { + // emit(ChatsHistoryFail()); + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } + + if (event is RestartChatsHistory) { + page = 1; + lastPage = null; + chats.clear(); + chatsInDates.clear(); + emit(ChatsHistoryInitial()); + } + }); + } +} diff --git a/lib/ui/screens/library/bloc/chats_history_event.dart b/lib/ui/screens/library/bloc/chats_history_event.dart new file mode 100644 index 0000000..c39fff8 --- /dev/null +++ b/lib/ui/screens/library/bloc/chats_history_event.dart @@ -0,0 +1,39 @@ +part of 'chats_history_bloc.dart'; + +sealed class ChatsHistoryEvent extends Equatable { + const ChatsHistoryEvent(); + + @override + List get props => []; +} + +class GetAllChats extends ChatsHistoryEvent { + final String? search; + final String? date; + final bool archive; + final String type; + + const GetAllChats( + {this.search, this.date, this.archive = false, required this.type}); +} + +class AddChat extends ChatsHistoryEvent { + final Chats chats; + + const AddChat({required this.chats}); +} + +class RemoveChat extends ChatsHistoryEvent { + final Chats chats; + final bool withCall; + + const RemoveChat({required this.chats, this.withCall = true}); +} + +class RemoveAll extends ChatsHistoryEvent { + final bool archive; + + const RemoveAll({required this.archive}); +} + +class RestartChatsHistory extends ChatsHistoryEvent {} diff --git a/lib/ui/screens/library/bloc/chats_history_state.dart b/lib/ui/screens/library/bloc/chats_history_state.dart new file mode 100644 index 0000000..3024a0f --- /dev/null +++ b/lib/ui/screens/library/bloc/chats_history_state.dart @@ -0,0 +1,22 @@ +part of 'chats_history_bloc.dart'; + +sealed class ChatsHistoryState extends Equatable { + final List chatsInDates; + + const ChatsHistoryState({this.chatsInDates = const []}); + + @override + List get props => [chatsInDates]; +} + +final class ChatsHistoryInitial extends ChatsHistoryState {} + +final class ChatsHistoryLoading extends ChatsHistoryState { + const ChatsHistoryLoading({super.chatsInDates}); +} + +final class ChatsHistorySuccess extends ChatsHistoryState { + const ChatsHistorySuccess({super.chatsInDates}); +} + +final class ChatsHistoryFail extends ChatsHistoryState {} diff --git a/lib/ui/screens/library/cubit/chat_row_edit_cubit.dart b/lib/ui/screens/library/cubit/chat_row_edit_cubit.dart new file mode 100644 index 0000000..ba7b71d --- /dev/null +++ b/lib/ui/screens/library/cubit/chat_row_edit_cubit.dart @@ -0,0 +1,26 @@ +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/data/repository/chatbot_repository.dart'; + +part 'chat_row_edit_state.dart'; + +class ChatRowEditCubit extends Cubit { + ChatRowEditCubit() : super(ChatRowEditInitial()); + + Future editTitle( + {required final int id, required final String title}) async { + emit(ChatRowEditLoading()); + try { + await ChatbotRepository.editChat(id: id, title: title); + + emit(ChatRowEditSuccess()); + } on DioException catch (e) { + emit(ChatRowEditFail()); + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } +} diff --git a/lib/ui/screens/library/cubit/chat_row_edit_state.dart b/lib/ui/screens/library/cubit/chat_row_edit_state.dart new file mode 100644 index 0000000..a497c3f --- /dev/null +++ b/lib/ui/screens/library/cubit/chat_row_edit_state.dart @@ -0,0 +1,16 @@ +part of 'chat_row_edit_cubit.dart'; + +sealed class ChatRowEditState extends Equatable { + const ChatRowEditState(); + + @override + List get props => []; +} + +final class ChatRowEditInitial extends ChatRowEditState {} + +final class ChatRowEditLoading extends ChatRowEditState {} + +final class ChatRowEditSuccess extends ChatRowEditState {} + +final class ChatRowEditFail extends ChatRowEditState {} diff --git a/lib/ui/screens/library/cubit/handle_archive_cubit.dart b/lib/ui/screens/library/cubit/handle_archive_cubit.dart new file mode 100644 index 0000000..21fdff0 --- /dev/null +++ b/lib/ui/screens/library/cubit/handle_archive_cubit.dart @@ -0,0 +1,39 @@ +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/data/repository/chatbot_repository.dart'; + +part 'handle_archive_state.dart'; + +class HandleArchiveCubit extends Cubit { + HandleArchiveCubit() : super(HandleArchiveInitial()); + + Future addToArchive(final int id) async { + emit(HandleArchiveLoading()); + try { + await ChatbotRepository.archiveChat(id, true); + + emit(HandleArchiveSuccess()); + } on DioException catch (e) { + emit(HandleArchiveFail()); + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } + + Future removeFromArchive(final int id) async { + emit(HandleArchiveLoading()); + try { + await ChatbotRepository.archiveChat(id, false); + + emit(HandleArchiveSuccess()); + } on DioException catch (e) { + emit(HandleArchiveFail()); + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } +} diff --git a/lib/ui/screens/library/cubit/handle_archive_state.dart b/lib/ui/screens/library/cubit/handle_archive_state.dart new file mode 100644 index 0000000..01bd7a4 --- /dev/null +++ b/lib/ui/screens/library/cubit/handle_archive_state.dart @@ -0,0 +1,16 @@ +part of 'handle_archive_cubit.dart'; + +sealed class HandleArchiveState extends Equatable { + const HandleArchiveState(); + + @override + List get props => []; +} + +final class HandleArchiveInitial extends HandleArchiveState {} + +final class HandleArchiveLoading extends HandleArchiveState {} + +final class HandleArchiveSuccess extends HandleArchiveState {} + +final class HandleArchiveFail extends HandleArchiveState {} diff --git a/lib/ui/screens/library/library_screen.dart b/lib/ui/screens/library/library_screen.dart new file mode 100644 index 0000000..94a1edf --- /dev/null +++ b/lib/ui/screens/library/library_screen.dart @@ -0,0 +1,644 @@ +// ignore_for_file: deprecated_member_use_from_same_package, use_build_context_synchronously + +import 'dart:math'; + +import 'package:easy_debounce/easy_debounce.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/utils/date_time.dart'; +import 'package:hoshan/core/utils/strings.dart'; +import 'package:hoshan/data/model/ai/chats_history_model.dart'; +import 'package:hoshan/data/model/empty_states_enum.dart'; +import 'package:hoshan/data/model/popup_menu_model.dart'; +import 'package:hoshan/ui/screens/main/home_page.dart'; +import 'package:hoshan/ui/screens/library/bloc/chats_history_bloc.dart'; +import 'package:hoshan/ui/screens/library/cubit/handle_archive_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/screens/library/cubit/chat_row_edit_cubit.dart'; +import 'package:hoshan/ui/widgets/components/button/circle_icon_btn.dart'; +import 'package:hoshan/ui/widgets/components/dialog/dialog_handler.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:hoshan/ui/widgets/components/text/auth_text_field.dart'; +import 'package:hoshan/ui/widgets/components/text/search_text_field.dart'; +import 'package:hoshan/ui/widgets/sections/empty/empty_states.dart'; +import 'package:hoshan/ui/widgets/sections/header/primary_appbar.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; +import 'package:hoshan/ui/widgets/sections/loading/listview_placeholder.dart'; +import 'package:shamsi_date/shamsi_date.dart'; + +class LibraryScreen extends StatefulWidget { + final Function(Chats chat)? onTap; + final String type; + const LibraryScreen({super.key, required this.type, this.onTap}); + + @override + State createState() => _LibraryScreenState(); +} + +class _LibraryScreenState extends State { + @override + void dispose() { + super.dispose(); + EasyDebounce.cancelAll(); + } + + ValueNotifier date = ValueNotifier(null); + Jalali? dateJalali; + final TextEditingController searchTextController = TextEditingController(); + bool archive = false; + final ScrollController scrollController = ScrollController(); + @override + void initState() { + super.initState(); + + scrollController.addListener( + () { + if (scrollController.position.pixels == + scrollController.position.maxScrollExtent && + context.read().state is ChatsHistorySuccess) { + context.read().add(GetAllChats( + type: widget.type, + archive: archive, + date: date.value, + search: searchTextController.text)); + } + }, + ); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + PrimaryAppbar( + context, + onBack: () { + Scaffold.of(context).closeDrawer(); + }, + actions: [ + const SizedBox( + width: 16, + ), + GestureDetector( + onTap: () async { + await DialogHandler(context: context).showDeleteItem( + title: 'همه نتایج جستجو پاک شوند؟', + description: 'با این کار اطلاعات شما ازبین خواهد رفت.', + onConfirm: () { + context + .read() + .add(RemoveAll(archive: archive)); + }, + ); + }, + child: SizedBox( + width: 24, + height: 24, + child: Assets.icon.outline.trash + .svg(color: Theme.of(context).colorScheme.onSurface))), + const SizedBox( + width: 24, + ), + GestureDetector( + onTap: () async { + if (context.read().state + is ChatsHistoryLoading) { + return; + } + archive = !archive; + ChatsHistoryBloc.page = 1; + ChatsHistoryBloc.lastPage = null; + context.read().add(GetAllChats( + type: widget.type, + search: searchTextController.text, + date: date.value, + archive: archive)); + setState(() {}); + }, + child: SizedBox( + width: 24, + height: 24, + child: (archive + ? Assets.icon.outline.directSend + : Assets.icon.outline.directInbox) + .svg(color: Theme.of(context).colorScheme.onSurface))), + const SizedBox( + width: 16, + ), + ], + ), + Expanded( + child: RefreshIndicator( + onRefresh: () async { + ChatsHistoryBloc.page = 1; + ChatsHistoryBloc.lastPage = null; + context.read().add(GetAllChats( + type: widget.type, + search: searchTextController.text, + date: date.value, + archive: archive)); + scrollController.jumpTo(0); + }, + backgroundColor: Theme.of(context).colorScheme.surface, + color: Theme.of(context).colorScheme.primary, + child: SingleChildScrollView( + controller: scrollController, + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics()), + child: Column( + children: [ + SearchTextField( + focusNode: searchFocus, + controller: searchTextController, + suffixIcon: ValueListenableBuilder( + valueListenable: date, + builder: (context, d, _) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (d != null) + Transform.scale( + scale: 0.8, + child: Transform.rotate( + angle: pi / 4, + child: CircleIconBtn( + icon: Assets.icon.outline.add, + color: context + .read() + .isDark() + ? AppColors.black[900] + : AppColors.secondryColor[50], + iconColor: + AppColors.secondryColor.defaultShade, + onTap: () { + date.value = null; + dateJalali = null; + ChatsHistoryBloc.page = 1; + ChatsHistoryBloc.lastPage = null; + context.read().add( + GetAllChats( + type: widget.type, + search: + searchTextController.text, + date: date.value, + archive: archive)); + return; + }, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(12), + child: GestureDetector( + onTap: () async { + await DialogHandler(context: context) + .showDatePicker( + dateCounts: 1, + onConfirm: (p0) { + if (p0.isEmpty) { + if (date.value != null) { + date.value = null; + dateJalali = null; + ChatsHistoryBloc.page = 1; + ChatsHistoryBloc.lastPage = null; + context + .read() + .add(GetAllChats( + type: widget.type, + search: searchTextController + .text, + date: date.value, + archive: archive)); + } + + return; + } + + dateJalali = p0.first; + DateTime miladiDate = + dateJalali!.toDateTime(); + date.value = + '${miladiDate.year}-${miladiDate.month}-${miladiDate.day}'; + ChatsHistoryBloc.page = 1; + ChatsHistoryBloc.lastPage = null; + context.read().add( + GetAllChats( + type: widget.type, + search: + searchTextController.text, + date: date.value, + archive: archive)); + }, + ); + }, + child: Assets.icon.outline.filter.svg(), + ), + ), + ], + ); + }), + onChanged: (searchText) { + if (searchText.isEmpty) { + EasyDebounce.cancelAll(); + ChatsHistoryBloc.page = 1; + ChatsHistoryBloc.lastPage = null; + context.read().add(GetAllChats( + type: widget.type, + date: date.value, + archive: archive)); + return; + } + EasyDebounce.debounce( + 'my-debouncer', // <-- An ID for this particular debouncer + const Duration( + seconds: 1), // <-- The debounce duration + () { + ChatsHistoryBloc.page = 1; + ChatsHistoryBloc.lastPage = null; + context.read().add(GetAllChats( + type: widget.type, + search: searchText, + date: date.value, + archive: archive)); + } // <-- The target method + ); + }, + ), + BlocConsumer( + listener: (context, state) {}, + builder: (context, state) { + if (state is ChatsHistorySuccess || + state is ChatsHistoryLoading) { + if (state.chatsInDates.isEmpty) { + return Center( + child: EmptyStates.getEmptyState( + status: archive + ? EmptyStatesEnum.archive + : EmptyStatesEnum.messages), + ); + } + return SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Column( + children: [ + ListView.builder( + itemCount: state.chatsInDates.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, allIndex) { + return state + .chatsInDates[allIndex].chats.isEmpty + ? const SizedBox() + : Column( + children: [ + if (allIndex != 0) + const SizedBox( + height: 8, + ), + Padding( + padding: + const EdgeInsets.symmetric( + horizontal: 12.0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + Text( + state.chatsInDates[allIndex] + .title, + style: AppTextStyles.body4 + .copyWith( + color: Theme.of( + context) + .colorScheme + .onSurface, + fontWeight: + FontWeight + .bold), + ), + ], + ), + ), + ListView.builder( + itemCount: state + .chatsInDates[allIndex] + .chats + .length, + shrinkWrap: true, + physics: + const NeverScrollableScrollPhysics(), + itemBuilder: + (context, chatIndex) { + final chat = state + .chatsInDates[allIndex] + .chats[chatIndex]; + return chatRow(context, chat); + }, + ), + ], + ); + }, + ), + if (state is ChatsHistoryLoading) + Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0), + child: LinearProgressIndicator( + color: AppColors.primaryColor.defaultShade, + borderRadius: BorderRadius.circular(16), + ), + ), + const SizedBox( + height: 36, + ) + ], + ), + ); + } + + return ListviewPlaceholder( + child: Container( + width: MediaQuery.sizeOf(context).width, + height: 58, + margin: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16)), + )); + }, + ), + ], + ), + ), + ), + ), + ], + ); + } + + Widget chatRow(BuildContext context, Chats chatModel) { + final List popups = [ + PopupMenuModel( + id: 0, title: 'تغییر نام', icon: Assets.icon.outline.edit2), + // PopupMenuModel( + // id: 1, title: 'به اشتراک گذاری', icon: Assets.icon.outline.share), + PopupMenuModel( + id: 2, + title: archive ? 'خارج کردن از آرشیو‌ها' : 'آرشیو کردن', + icon: archive + ? Assets.icon.outline.directSend + : Assets.icon.outline.directInbox), + PopupMenuModel(id: 3, title: 'پاک کردن', icon: Assets.icon.outline.trash), + ]; + late final TextEditingController editingController = TextEditingController( + text: chatModel.title!.replaceAll("\"", ''), + ); + ValueNotifier isEdit = ValueNotifier(false); + + late Chats chat = chatModel; + + return MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => ChatRowEditCubit()), + BlocProvider( + create: (context) => HandleArchiveCubit()), + ], + child: BlocConsumer( + listener: (context, archState) { + if (archState is HandleArchiveSuccess) { + context + .read() + .add(RemoveChat(chats: chat, withCall: false)); + } + }, + builder: (context, archState) { + if (archState is HandleArchiveLoading) { + return DefaultPlaceHolder( + child: Container( + width: MediaQuery.sizeOf(context).width, + height: 58, + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + decoration: BoxDecoration( + color: Colors.white, borderRadius: BorderRadius.circular(8)), + )); + } + return BlocBuilder( + builder: (context, state) { + return GestureDetector( + onTap: () async { + // Scaffold.of(context).closeDrawer(); + // await Future.delayed(Duration(milliseconds: 300)); + widget.onTap?.call(chat); + }, + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 12), + margin: + const EdgeInsets.symmetric(vertical: 4, horizontal: 4), + width: MediaQuery.sizeOf(context).width, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8)), + child: ValueListenableBuilder( + valueListenable: isEdit, + builder: (context, edit, _) { + return Row( + children: [ + state is ChatRowEditLoading + ? SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + color: + AppColors.primaryColor.defaultShade, + ), + ) + : edit + ? GestureDetector( + onTap: () async { + await context + .read() + .editTitle( + id: chat.id!, + title: + editingController.text); + chat = chat.copyWith( + title: editingController.text); + isEdit.value = false; + }, + child: Assets.icon.outline.tickCircle + .svg( + color: AppColors.gray[context + .read() + .isDark() + ? 600 + : 900])) + : PopupMenuButton( + tooltip: '', + offset: const Offset(0, 18), + onSelected: (value) async { + switch (value.id) { + case 0: + isEdit.value = true; + break; + case 1: + break; + case 2: + archive + ? await context + .read< + HandleArchiveCubit>() + .removeFromArchive( + chat.id!) + : await context + .read< + HandleArchiveCubit>() + .addToArchive(chat.id!); + break; + case 3: + // widget.onDelete.call(); + await DialogHandler( + context: context) + .showDeleteItem( + title: 'تاریخچه گفتگو پاک شود؟', + description: + 'با این کار اطلاعات شما از بین خواهد رفت.', + onConfirm: () { + context + .read() + .add(RemoveChat( + chats: chat)); + }, + ); + break; + default: + } + }, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(8)), + itemBuilder: (BuildContext context) { + return >[ + ...List.generate( + popups.length, + (index) => + PopupMenuItem( + value: popups[index], + height: 32, + child: Directionality( + textDirection: + TextDirection.rtl, + child: ListTile( + minTileHeight: 32, + title: Text( + popups[index].title, + style: AppTextStyles + .body5 + .copyWith( + color: Theme.of( + context) + .colorScheme + .onSurface, + fontWeight: + FontWeight + .bold), + ), + leading: popups[index] + .icon! + .svg( + color: AppColors + .secondryColor + .defaultShade, + width: 16, + height: 16), + )), + ), + ) + ]; + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10.0), + child: Assets.icon.outline.more.svg( + color: AppColors.gray[context + .read() + .isDark() + ? 600 + : 900]), + )), + const SizedBox( + width: 4, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + edit + ? AuthTextField( + controller: editingController, + maxLines: 4, + minLines: 4, + ) + : Directionality( + textDirection: + chat.title!.startsWithEnglish() + ? TextDirection.ltr + : TextDirection.rtl, + child: Text( + chat.title!.replaceAll("\"", ''), + style: AppTextStyles.body5.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox( + height: 4, + ), + Text( + DateTimeUtils + .convertStringIsoToStringInFormatted( + chat.createdAt!), + style: AppTextStyles.body6.copyWith( + color: AppColors.gray[context + .read() + .isDark() + ? 600 + : 900]), + ) + ], + ), + ), + if (widget.type != 'llm') + Row( + children: [ + const SizedBox( + width: 8, + ), + ImageNetwork( + width: 32, + height: 32, + url: chat.bot!.image, + radius: 360, + ), + ], + ) + ], + ); + }), + ), + ); + }, + ); + }, + ), + ); + } +} diff --git a/lib/ui/screens/main/assistant/assistant_screen.dart b/lib/ui/screens/main/assistant/assistant_screen.dart new file mode 100644 index 0000000..255e4bd --- /dev/null +++ b/lib/ui/screens/main/assistant/assistant_screen.dart @@ -0,0 +1,145 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/ui/screens/main/assistant/bloc/personal_assistants_bloc.dart'; +import 'package:hoshan/ui/screens/main/assistant/global_assistants_screen.dart'; +import 'package:hoshan/ui/screens/main/assistant/personal_assistants_screen.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/widgets/components/button/tab_btn.dart'; + +class AssistantScreen extends StatefulWidget { + const AssistantScreen({super.key}); + + @override + State createState() => _AssistantScreenState(); +} + +class _AssistantScreenState extends State + with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + int selectedIndex = 0; + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + floatingActionButton: Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + if ((selectedIndex == 1 && + context.watch().state + is PersonalAssistantsEmpty)) + IgnorePointer( + child: Container( + padding: const EdgeInsets.only(right: 10, bottom: 12), + child: + Assets.icon.gif.flash.image(fit: BoxFit.fill, scale: 1.5), + ), + ), + FloatingActionButton( + heroTag: 'create-assistants-fb', + onPressed: () { + context.go(Routes.createAssistant); + }, + backgroundColor: AppColors.primaryColor.defaultShade, + shape: const CircleBorder(), + child: Assets.icon.bold.createAssistant + .svg(color: Colors.white, width: 20, height: 20), + ) + .animate( + autoPlay: true, + onPlay: (controller) => controller.repeat(reverse: true), + ) + .scale( + begin: const Offset(1, 1), + end: const Offset(1.2, 1.2), + duration: 600.ms, + curve: Curves.easeInOut, + delay: 1.seconds), + ], + ), + body: Column( + children: [ + tabBars(context), + Expanded( + child: IndexedStack( + index: selectedIndex, + children: const [ + GlobalAssistantsScreen(), + PersonalAssistantsScreen() + ], + ), + ) + ], + ), + ); + } + + Widget tabBars(BuildContext context) { + return Directionality( + textDirection: TextDirection.rtl, + child: Column( + children: [ + // AiBanner(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + margin: const EdgeInsets.symmetric(vertical: 8), + decoration: + BoxDecoration(color: Theme.of(context).scaffoldBackgroundColor), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: InkWell( + borderRadius: BorderRadius.circular(10), + onTap: () => setState(() => selectedIndex = 0), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 0.0, horizontal: 0.0), + child: TabBtn( + title: 'دستیارهای هوشان', + icon: Assets.icon.bold.globalAssistant, + active: selectedIndex == 0, + click: () => setState(() => selectedIndex = 0), + ), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: InkWell( + borderRadius: BorderRadius.circular(10), + onTap: () => setState(() => selectedIndex = 1), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 0.0, horizontal: 0.0), + child: TabBtn( + title: 'دستیارهای من', + icon: Assets.icon.bold.myAssistant, + active: selectedIndex == 1, + click: () => setState(() => selectedIndex = 1), + ), + ), + ), + ), + ], + ), + Container( + height: 2, + width: double.infinity, + color: AppColors.gray.defaultShade, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/screens/main/assistant/bloc/create_assistant_bloc.dart b/lib/ui/screens/main/assistant/bloc/create_assistant_bloc.dart new file mode 100644 index 0000000..1b52e10 --- /dev/null +++ b/lib/ui/screens/main/assistant/bloc/create_assistant_bloc.dart @@ -0,0 +1,33 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/create_assistant_request_model.dart'; +import 'package:hoshan/data/repository/bot_repository.dart'; + +part 'create_assistant_event.dart'; +part 'create_assistant_state.dart'; + +class CreateAssistantBloc + extends Bloc { + CreateAssistantBloc() : super(CreateAssistantInitial()) { + on((event, emit) async { + if (event is CreateAnAssistant) { + emit(CreateAssistantLoading()); + try { + if (event.id != null) { + await BotRepository.editBot(id: event.id!, model: event.model); + } else { + await BotRepository.createBot(model: event.model); + } + emit(CreateAssistantSuccess(isEdit: event.id != null)); + } on DioException catch (e) { + emit(const CreateAssistantFail()); + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } + }); + } +} diff --git a/lib/ui/screens/main/assistant/bloc/create_assistant_event.dart b/lib/ui/screens/main/assistant/bloc/create_assistant_event.dart new file mode 100644 index 0000000..b8468ef --- /dev/null +++ b/lib/ui/screens/main/assistant/bloc/create_assistant_event.dart @@ -0,0 +1,15 @@ +part of 'create_assistant_bloc.dart'; + +sealed class CreateAssistantEvent extends Equatable { + const CreateAssistantEvent(); + + @override + List get props => []; +} + +class CreateAnAssistant extends CreateAssistantEvent { + final CreateAssistantRequestModel model; + final int? id; + + const CreateAnAssistant({required this.model, this.id}); +} diff --git a/lib/ui/screens/main/assistant/bloc/create_assistant_state.dart b/lib/ui/screens/main/assistant/bloc/create_assistant_state.dart new file mode 100644 index 0000000..8596e45 --- /dev/null +++ b/lib/ui/screens/main/assistant/bloc/create_assistant_state.dart @@ -0,0 +1,24 @@ +part of 'create_assistant_bloc.dart'; + +sealed class CreateAssistantState extends Equatable { + const CreateAssistantState(); + + @override + List get props => []; +} + +final class CreateAssistantInitial extends CreateAssistantState {} + +final class CreateAssistantLoading extends CreateAssistantState {} + +final class CreateAssistantSuccess extends CreateAssistantState { + final bool isEdit; + + const CreateAssistantSuccess({required this.isEdit}); +} + +final class CreateAssistantFail extends CreateAssistantState { + final String? message; + + const CreateAssistantFail({this.message}); +} diff --git a/lib/ui/screens/main/assistant/bloc/global_assistants_bloc.dart b/lib/ui/screens/main/assistant/bloc/global_assistants_bloc.dart new file mode 100644 index 0000000..f1e7437 --- /dev/null +++ b/lib/ui/screens/main/assistant/bloc/global_assistants_bloc.dart @@ -0,0 +1,60 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/data/model/global_assistant_bots_model.dart'; +import 'package:hoshan/data/repository/bot_repository.dart'; + +part 'global_assistants_event.dart'; +part 'global_assistants_state.dart'; + +class GlobalAssistantsBloc + extends Bloc { + GlobalAssistantsBloc() : super(GlobalAssistantsInitial()) { + on((event, emit) async { + if (event is GetGlobalAssistants) { + emit(GlobalAssistantsLoading()); + try { + final mark = event.category == -2; + final catId = event.category == -1 || event.category == -2 + ? null + : event.category; + final cats = await BotRepository.getGlobalAssistant( + marked: mark, categorieId: catId); + emit(GlobalAssistantsSuccess( + assistants: cats.categories!, category: event.category)); + } on DioException catch (e) { + emit(GlobalAssistantsFail()); + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } + + if (event is ChangeGlobalAssistantBot) { + final updatedList = List.of(state.assistants); + final index = + updatedList[event.oldAssistantsIndex].bots!.indexOf(event.oldBot); + updatedList[event.oldAssistantsIndex].bots![index] = event.newBot; + emit(GlobalAssistantsSuccess( + category: state.category, assistants: updatedList)); + } + + // if (event is RemoveGlobalAssistantBot) { + // try { + // final updatedList = List.of(state.assistants); + // final index = + // updatedList[event.oldAssistantsIndex].bots!.indexOf(event.oldBot); + // updatedList[event.oldAssistantsIndex].bots!.removeAt(index); + // emit(GlobalAssistantsSuccess( + // category: state.category, assistants: updatedList)); + // } catch (e) { + // if (kDebugMode) { + // print(' Error is: $e'); + // } + // } + // } + }); + } +} diff --git a/lib/ui/screens/main/assistant/bloc/global_assistants_event.dart b/lib/ui/screens/main/assistant/bloc/global_assistants_event.dart new file mode 100644 index 0000000..1b98319 --- /dev/null +++ b/lib/ui/screens/main/assistant/bloc/global_assistants_event.dart @@ -0,0 +1,33 @@ +part of 'global_assistants_bloc.dart'; + +sealed class GlobalAssistantsEvent extends Equatable { + const GlobalAssistantsEvent(); + + @override + List get props => []; +} + +class GetGlobalAssistants extends GlobalAssistantsEvent { + final int? category; + + const GetGlobalAssistants({this.category}); +} + +class ChangeGlobalAssistantBot extends GlobalAssistantsEvent { + final int oldAssistantsIndex; + final Bots oldBot; + final Bots newBot; + + const ChangeGlobalAssistantBot( + {required this.oldAssistantsIndex, + required this.oldBot, + required this.newBot}); +} + +// class RemoveGlobalAssistantBot extends GlobalAssistantsEvent { +// final int oldAssistantsIndex; +// final Bots oldBot; + +// const RemoveGlobalAssistantBot( +// {required this.oldAssistantsIndex, required this.oldBot}); +// } diff --git a/lib/ui/screens/main/assistant/bloc/global_assistants_state.dart b/lib/ui/screens/main/assistant/bloc/global_assistants_state.dart new file mode 100644 index 0000000..6d80db5 --- /dev/null +++ b/lib/ui/screens/main/assistant/bloc/global_assistants_state.dart @@ -0,0 +1,22 @@ +part of 'global_assistants_bloc.dart'; + +sealed class GlobalAssistantsState extends Equatable { + final int? category; + + final List assistants; + + const GlobalAssistantsState({this.category, this.assistants = const []}); + + @override + List get props => [assistants, category ?? -1]; +} + +final class GlobalAssistantsInitial extends GlobalAssistantsState {} + +final class GlobalAssistantsLoading extends GlobalAssistantsState {} + +final class GlobalAssistantsSuccess extends GlobalAssistantsState { + const GlobalAssistantsSuccess({super.assistants, required super.category}); +} + +final class GlobalAssistantsFail extends GlobalAssistantsState {} diff --git a/lib/ui/screens/main/assistant/bloc/personal_assistants_bloc.dart b/lib/ui/screens/main/assistant/bloc/personal_assistants_bloc.dart new file mode 100644 index 0000000..4a0188f --- /dev/null +++ b/lib/ui/screens/main/assistant/bloc/personal_assistants_bloc.dart @@ -0,0 +1,34 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/personal_assistants_bots.dart'; +import 'package:hoshan/data/repository/bot_repository.dart'; + +part 'personal_assistants_event.dart'; +part 'personal_assistants_state.dart'; + +class PersonalAssistantsBloc + extends Bloc { + PersonalAssistantsBloc() : super(PersonalAssistantsInitial()) { + on((event, emit) async { + if (event is GetAll) { + emit(PersonalAssistantsLoading()); + try { + final response = await BotRepository.getPersonalAssistant(); + if (response.personalAssistants != null && + response.personalAssistants!.isNotEmpty) { + emit(PersonalAssistantsSuccess( + personalAssistants: response.personalAssistants!)); + } else { + emit(PersonalAssistantsEmpty()); + } + } on DioException catch (e) { + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } + }); + } +} diff --git a/lib/ui/screens/main/assistant/bloc/personal_assistants_event.dart b/lib/ui/screens/main/assistant/bloc/personal_assistants_event.dart new file mode 100644 index 0000000..ed8a6c3 --- /dev/null +++ b/lib/ui/screens/main/assistant/bloc/personal_assistants_event.dart @@ -0,0 +1,10 @@ +part of 'personal_assistants_bloc.dart'; + +sealed class PersonalAssistantsEvent extends Equatable { + const PersonalAssistantsEvent(); + + @override + List get props => []; +} + +class GetAll extends PersonalAssistantsEvent {} diff --git a/lib/ui/screens/main/assistant/bloc/personal_assistants_state.dart b/lib/ui/screens/main/assistant/bloc/personal_assistants_state.dart new file mode 100644 index 0000000..e606f5f --- /dev/null +++ b/lib/ui/screens/main/assistant/bloc/personal_assistants_state.dart @@ -0,0 +1,21 @@ +part of 'personal_assistants_bloc.dart'; + +sealed class PersonalAssistantsState extends Equatable { + final List personalAssistants; + const PersonalAssistantsState({this.personalAssistants = const []}); + + @override + List get props => [personalAssistants]; +} + +final class PersonalAssistantsInitial extends PersonalAssistantsState {} + +final class PersonalAssistantsLoading extends PersonalAssistantsState {} + +final class PersonalAssistantsEmpty extends PersonalAssistantsState {} + +final class PersonalAssistantsSuccess extends PersonalAssistantsState { + const PersonalAssistantsSuccess({super.personalAssistants}); +} + +final class PersonalAssistantsFail extends PersonalAssistantsState {} diff --git a/lib/ui/screens/main/assistant/create_assistant_page.dart b/lib/ui/screens/main/assistant/create_assistant_page.dart new file mode 100644 index 0000000..d1e8c7d --- /dev/null +++ b/lib/ui/screens/main/assistant/create_assistant_page.dart @@ -0,0 +1,1231 @@ +// ignore_for_file: use_build_context_synchronously, deprecated_member_use_from_same_package + +import 'dart:math'; + +import 'package:animated_custom_dropdown/custom_dropdown.dart'; +import 'package:cross_file/cross_file.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/core/services/file_manager/pick_file_services.dart'; +import 'package:hoshan/core/utils/strings.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/data/model/assistant_personal_info_model.dart'; +import 'package:hoshan/data/model/create_assistant_request_model.dart'; +import 'package:hoshan/data/model/edittext_state_model.dart'; +import 'package:hoshan/data/model/tools_categories_model.dart'; +import 'package:hoshan/ui/screens/main/assistant/bloc/create_assistant_bloc.dart'; +import 'package:hoshan/ui/screens/main/assistant/bloc/personal_assistants_bloc.dart'; +import 'package:hoshan/ui/screens/main/assistant/cubit/delete_assistant_cubit.dart'; +import 'package:hoshan/ui/screens/main/home/bloc/bots_bloc.dart'; +import 'package:hoshan/ui/screens/main/forum/cubit/category_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/dialog/dialog_handler.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; +import 'package:hoshan/ui/widgets/components/text/filled_text_field.dart'; +import 'package:hoshan/ui/widgets/components/text/labeled_text_field.dart'; +import 'package:hoshan/ui/widgets/sections/header/reversible_appbar.dart'; +import 'package:string_validator/string_validator.dart'; + +class CreateAssistantPage extends StatefulWidget { + final AssistantPersonalInfo? info; + const CreateAssistantPage({super.key, this.info}); + + @override + State createState() => _CreateAssistantPageState(); +} + +class _CreateAssistantPageState extends State { + final ValueNotifier selectedImageFileLoading = ValueNotifier(false); + final ValueNotifier> selectedFiles = ValueNotifier([]); + final ValueNotifier> links = ValueNotifier([]); + // int? botId; + // final ValueNotifier botIdError = ValueNotifier(false); + int? categoryId; + ValueNotifier categoryIdError = ValueNotifier(false); + Bots? initialItem; + + ValueNotifier isPublic = ValueNotifier(false); + + final EdittextStateModel linkFormState = + EdittextStateModel(hintText: 'لینک‌ خود را وارد کنید.'); + final EdittextStateModel userFormState = EdittextStateModel( + label: 'خالق دستیار ', + hintText: 'یک نام کاربری برای خود انتخاب کنید.', + ); + final EdittextStateModel nameFormState = EdittextStateModel( + label: 'نام دستیار', + hintText: 'یک نام برای دستیار خود انتخاب کنید.', + tooltipHint: + 'در این بخش باید نامی (شامل حروف فارسی و انگلیسی و علائم و اعداد) برای دستیار هوش مصنوعی خود انتخاب کنید. این نام میتواند متناسب با عملکرد یا هدف دستیار باشد. انتخاب نام مناسب به شما کمک می‌کند تا دستیارهای مختلف خود را به راحتی مدیریت و شناسایی کنید.', + ); + final EdittextStateModel promptFormState = EdittextStateModel( + label: 'پرامپت و دستورالعمل ', + hintText: 'این دستیار قرار است چه کارهایی انجام دهد؟', + tooltipHint: + 'در این قسمت باید دقیقاً مشخص کنید که دستیار شما چه کارهایی را باید انجام دهد. این شامل دستور العمل ها، قوانین و محدودیتهایی است که باید رعایت شود. به عنوان مثال اگر دستیار شما برای کمک در نوشتن مقالات استفاده می شود میتوانید توضیح دهید که دستیار باید از منابع معتبر استفاده کند و اطلاعات را به صورت ساده و قابل فهم ارائه دهد. همچنین می توانید مدل را محدود کنید که در مورد مسائل دیگر جوابی ندهد. این قسمت بسیار حیاتی است زیرا تعیین می کند که دستیار چگونه باید رفتار کند و چه نوع خروجی هایی ارائه دهد.', + ); + final EdittextStateModel descriptionFormState = EdittextStateModel( + label: 'توضیحات', + hintText: 'توضیح دهید چه کارهایی از این بات بر می‌آید.', + tooltipHint: + 'در این قسمت میتوانید به کاربرانی که قرار است از این دستیار استفاده کنند توضیحاتی راجع به دستیار خود بدهید.'); + + bool checkReqs() { + // botIdError.value = botId == null; + categoryIdError.value = categoryId == null; + + return + // botIdError.value && + categoryIdError.value; + } + + @override + void initState() { + super.initState(); + if (widget.info != null) { + final info = widget.info!; + nameFormState.formController.text = info.name ?? ''; + descriptionFormState.formController.text = info.description ?? ''; + promptFormState.formController.text = info.prompt ?? ''; + if (info.links != null && info.links!.isNotEmpty) { + links.value.addAll(info.links!); + } + if (info.docs != null && info.docs!.isNotEmpty) { + for (var doc in info.docs!) { + DioService.downloadFile(doc).then((file) { + if (file != null) { + selectedFiles.value = [...selectedFiles.value, file]; + } + }); + } + } + isPublic.value = info.public ?? false; + try { + initialItem = BotsBloc.allBots.firstWhere( + (element) => element.name == widget.info!.model, + ); + } catch (e) { + if (kDebugMode) { + print('Error is: $e'); + } + } + + // botId = initialItem?.id; + // if (info.image != null) { + // selectedImageFileLoading.value = true; + // dio.downloadFile(info.image!).then((file) { + // if (file != null) { + // // selectedImageFile.value = file; + // selectedImageFileLoading.value = false; + // } + // }); + // } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: ReversibleAppbar( + context, + titleText: 'ساخت دستیار', + ), + body: Responsive(context).maxWidthInDesktop( + child: (contxet, maxWidth) => + BlocConsumer( + listener: (mContext, state) async { + if (state is CreateAssistantFail) { + SnackBarManager(context, id: 'createAssistantError').show( + status: SnackBarStatus.error, + message: state.message ?? 'خطا از طرف سرور'); + } + if (state is CreateAssistantSuccess) { + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().add(GetAll()); + + context.pop(); + DialogHandler(context: context).showCreateSuccess(); + }); + } + }, + builder: (context, state) => SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).colorScheme.error, + border: Border.all( + color: Theme.of(context) + .colorScheme + .onError + .withOpacity(0.3), + ), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + color: Theme.of(context).colorScheme.onError, + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'دستیارهای ایجادی شما فقط قابلیت چت دارند', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + textDirection: TextDirection.rtl, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + // if (UserInfoCubit.userInfoModel.username == null) + // Padding( + // padding: const EdgeInsets.symmetric(vertical: 16.0), + // child: + // BlocBuilder( + // builder: (context, state) { + // return Column( + // children: [ + // Stack( + // children: [ + // LabeledTextField( + // maxLines: 1, + // showLabel: false, + // justEnglish: true, + // stateController: userFormState, + // hintStyle: AppTextStyles.body4 + // .copyWith(color: AppColors.gray[700]), + // success: state is CheckUsernameSuccess + // ? Row( + // crossAxisAlignment: + // CrossAxisAlignment.start, + // children: [ + // Icon( + // Icons.check_circle, + // color: AppColors + // .green.defaultShade, + // size: 16, + // ), + // const SizedBox( + // width: 8, + // ), + // Expanded( + // child: Text( + // 'نام کاربری در دسترس است', + // style: AppTextStyles.body5 + // .copyWith( + // color: AppColors.green + // .defaultShade), + // ), + // ), + // ], + // ) + // : null, + // error: state is CheckUsernameFail + // ? Row( + // crossAxisAlignment: + // CrossAxisAlignment.start, + // children: [ + // Icon( + // Icons.warning_amber_rounded, + // color: + // AppColors.red.defaultShade, + // size: 16, + // ), + // const SizedBox( + // width: 8, + // ), + // Expanded( + // child: Text( + // 'نام کاربری قبلا انتخاب شده است', + // style: AppTextStyles.body5 + // .copyWith( + // color: AppColors.red + // .defaultShade), + // ), + // ), + // ], + // ) + // : null, + // suffix: Padding( + // padding: const EdgeInsets.all(8.0), + // child: Assets.icon.outline.profileTick + // .svg( + // color: state is CheckUsernameFail + // ? AppColors.red.defaultShade + // : state + // is CheckUsernameSuccess + // ? AppColors + // .green.defaultShade + // : AppColors.primaryColor + // .defaultShade), + // ), + // onChange: (usernameText) { + // if (usernameText.isEmpty) { + // return; + // } + // context + // .read() + // .loading(); + // EasyDebounce.debounce( + // 'my-username', // <-- An ID for this particular debouncer + // const Duration( + // seconds: + // 1), // <-- The debounce duration + // () { + // context + // .read() + // .check(usernameText); + // } // <-- The target method + // ); + // }, + // ), + // Positioned( + // top: 0, + // right: 16, + // child: titleWithHint( + // text: userFormState.label ?? '', + // hint: userFormState.tooltipHint, + // )) + // ], + // ), + // if (state is CheckUsernameLoading) + // Padding( + // padding: const EdgeInsets.symmetric( + // horizontal: 18, vertical: 4), + // child: Row( + // crossAxisAlignment: + // CrossAxisAlignment.start, + // children: [ + // SizedBox( + // width: 16, + // height: 16, + // child: CircularProgressIndicator( + // color: AppColors + // .primaryColor.defaultShade, + // ), + // ), + // const SizedBox( + // width: 8, + // ), + // Expanded( + // child: Text( + // 'درحال بررسی', + // style: AppTextStyles.body5.copyWith( + // color: AppColors + // .primaryColor.defaultShade), + // ), + // ), + // ], + // ), + // ) + // ], + // ); + // }, + // ), + // ), + + // Padding( + // padding: const EdgeInsets.symmetric(vertical: 16.0), + // child: Column( + // children: [ + // GestureDetector( + // onTap: () async => + // await BottomSheetHandler(context).showPickImage( + // onSelect: (file) { + // selectedImageFile.value = file; + // }, + // ), + // child: ValueListenableBuilder( + // valueListenable: selectedImageFile, + // builder: (context, img, _) { + // return ValueListenableBuilder( + // valueListenable: selectedImageFileLoading, + // builder: (context, loading, _) { + // return Container( + // width: 120, + // height: 120, + // decoration: BoxDecoration( + // color: Colors.white, + // borderRadius: + // BorderRadius.circular(16)), + // child: ClipRRect( + // borderRadius: + // BorderRadius.circular(16), + // child: loading + // ? SpinKitThreeBounce( + // color: AppColors.primaryColor + // .defaultShade, + // size: 18, + // ) + // : img != null + // ? CustomeImage( + // src: img.path, + // fit: BoxFit.cover, + // ) + // : Padding( + // padding: + // const EdgeInsets.all( + // 24), + // child: Assets.icon.outline + // .galleryAdd + // .svg(), + // ), + // ), + // ); + // }); + // }), + // ), + // Text( + // 'انتخاب تصویر برای دستیار', + // style: AppTextStyles.body4.copyWith( + // color: AppColors.primaryColor.defaultShade), + // ), + // const SizedBox( + // height: 16, + // ), + // if (selectedImageFileError) + // Text( + // 'باید برای دستیار خود یک عکس انتخاب کنید !', + // style: AppTextStyles.body4.copyWith( + // color: AppColors.red.defaultShade, + // fontWeight: FontWeight.bold), + // ), + // ], + // ), + // ), + + Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: LabeledTextField( + showLabel: false, + stateController: nameFormState, + hintStyle: AppTextStyles.body4 + .copyWith(color: AppColors.gray[700]), + onChange: (value) {}, + onValid: (value) { + if (value != null && value.isEmpty) { + return '! نام دستیار نباید خالی باشد'; + } + return null; + }, + maxLines: 1, + ), + ), + Positioned( + top: 0, + right: 16, + child: titleWithHint( + text: nameFormState.label ?? '', + hint: nameFormState.tooltipHint, + )) + ], + ), + BlocBuilder( + builder: (context, state) { + Categories? initialItem; + if (widget.info != null && categoryId == null) { + initialItem = state.categories.firstWhere( + (element) => element.id == widget.info!.category?.id, + ); + categoryId = widget.info!.category?.id; + } + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Column( + children: [ + ValueListenableBuilder( + valueListenable: categoryIdError, + builder: (context, err, _) { + return labeledCard( + error: err + ? 'باید برای دستیار خود یک دسته بندی انتخاب کنید !' + : null, + label: 'دسته‌بندی', + // hint: 'sssssssss', + child: Directionality( + textDirection: TextDirection.rtl, + child: CustomDropdown( + items: state.categories, + initialItem: initialItem, + // initialItems: ['item1'], + hintText: + 'نوع دستیار خود را بر اساس زمینه کاری تعیین کنید.', + listItemBuilder: (context, item, + isSelected, onItemSelect) { + return Text(item.name ?? '', + style: AppTextStyles.body4 + .copyWith( + color: Theme.of(context) + .colorScheme + .onSurface)); + }, + headerBuilder: + (context, selectedItem, enabled) => + Text( + selectedItem.name ?? '', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + + decoration: CustomDropdownDecoration( + expandedSuffixIcon: + Transform.rotate( + angle: pi / -2, + child: Assets + .icon.outline.arrowRight + .svg( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + closedSuffixIcon: Transform.rotate( + angle: pi / 2, + child: Assets + .icon.outline.arrowRight + .svg( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + hintStyle: AppTextStyles.body4 + .copyWith( + color: AppColors.gray[700]), + expandedFillColor: Theme.of(context) + .colorScheme + .surface, + closedFillColor: Theme.of(context) + .scaffoldBackgroundColor), + onChanged: (category) { + categoryId = category?.id; + }, + ), + ), + ); + }), + ], + ), + ); + }, + ), + + // Padding( + // padding: const EdgeInsets.symmetric(vertical: 16.0), + // child: ValueListenableBuilder( + // valueListenable: botIdError, + // builder: (context, err, _) { + // return labeledCard( + // error: err + // ? 'باید برای دستیار خود یک مدل انتخاب کنید !' + // : null, + // label: 'مدل زبانی LLM', + // hint: + // 'مدل‌های زبانی (LLM)، قلب دستیار هوش مصنوعی شما هستند. در این بخش از میان مدل‌های موجود در هوشان، مدلی را که می‌خواهید دستیار شما از آن برای پردازش زبان و تولید پاسخ‌ها استفاده کند، انتخاب کنید. برای مثال، اگر نیاز به دقت بالا در تولید محتوای علمی دارید، مدلی را انتخاب کنید که برای این نوع از سوالات بهینه شده باشد. دقت کنید که هر مدل ویژگی‌ها، توانایی‌ها و قیمت‌ها متفاوتی دارد. بنابراین انتخاب مدل بسیار مهم است.', + // child: Directionality( + // textDirection: TextDirection.rtl, + // child: CustomDropdown( + // initialItem: initialItem, + // decoration: CustomDropdownDecoration( + // expandedSuffixIcon: Transform.rotate( + // angle: pi / -2, + // child: Assets.icon.outline.arrowRight.svg( + // color: Theme.of(context) + // .colorScheme + // .onSurface), + // ), + // closedSuffixIcon: Transform.rotate( + // angle: pi / 2, + // child: Assets.icon.outline.arrowRight.svg( + // color: Theme.of(context) + // .colorScheme + // .onSurface), + // ), + // hintStyle: AppTextStyles.body4 + // .copyWith(color: AppColors.gray[700]), + // expandedFillColor: + // Theme.of(context).colorScheme.surface, + // closedFillColor: Theme.of(context) + // .scaffoldBackgroundColor), + // items: BotsBloc.createBots, + // headerBuilder: + // (context, selectedItem, enabled) => Row( + // children: [ + // ImageNetwork( + // url: selectedItem.image, + // radius: 360, + // width: 32, + // height: 32, + // color: Theme.of(context) + // .colorScheme + // .onSurface, + // ), + // const SizedBox( + // width: 8, + // ), + // Text( + // selectedItem.name ?? '', + // style: TextStyle( + // color: Theme.of(context) + // .colorScheme + // .onSurface), + // ) + // ], + // ), + // listItemBuilder: + // (context, item, isSelected, onItemSelect) { + // return Row( + // children: [ + // ImageNetwork( + // url: item.image, + // radius: 360, + // width: 32, + // height: 32, + // color: Theme.of(context) + // .colorScheme + // .onSurface, + // ), + // const SizedBox( + // width: 8, + // ), + // Text(item.name ?? '', + // style: TextStyle( + // color: Theme.of(context) + // .colorScheme + // .onSurface)) + // ], + // ); + // }, + // hintText: + // 'یک مدل زبانی بزرگ (LLM) انتخاب کنید.', + // onChanged: (item) { + // botId = item?.id; + // }, + // ), + // ), + // ); + // }), + // ), + + Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: LabeledTextField( + showLabel: false, + stateController: descriptionFormState, + onChange: (value) {}, + onValid: (value) { + if (value != null && value.isEmpty) { + return '! توضیحات دستیار نباید خالی باشد'; + } + return null; + }, + maxLines: 6, + minLines: 6, + hintStyle: AppTextStyles.body4 + .copyWith(color: AppColors.gray[700]), + ), + ), + Positioned( + top: 0, + right: 16, + child: titleWithHint( + text: 'توضیحات', + hint: + 'در این قسمت میتوانید به کاربرانی که قرار است از این دستیار استفاده کنند توضیحاتی راجع به دستیار خود بدهید.', + )) + ], + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Row( + children: [ + const Expanded(child: Divider()), + const SizedBox( + width: 4, + ), + Text( + 'پایگاه دانش دستیار', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + ], + ), + ), + Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: LabeledTextField( + showLabel: false, + stateController: promptFormState, + onChange: (value) {}, + onValid: (value) { + if (value != null && value.isEmpty) { + return '! پرامپت و دستورالعمل دستیار نباید خالی باشد'; + } + return null; + }, + maxLines: 6, + minLines: 6, + hintStyle: AppTextStyles.body4 + .copyWith(color: AppColors.gray[700]), + ), + ), + Positioned( + top: 0, + right: 16, + child: titleWithHint( + text: promptFormState.label ?? '', + hint: promptFormState.tooltipHint, + )) + ], + ), + labeledCard( + label: 'فایل‌های مرتبط ', + hint: + 'اگر فایل هایی دارید که شامل اطلاعات مرتبط با سوالات کاربران است می‌توانید آن‌ها را در این بخش آپلود کنید. سیستم به جای تحلیل کل فایل، فقط بخش‌های مرتبط با سوال کاربر را استخراج و استفاده می‌کند. به این ترتیب اگر کاربران سوالی مطرح کنند که اطلاعات آن در فایل‌های بارگذاری شده موجود باشد. دستیار می‌تواند به صورت هوشمند تکه‌های مرتبط را پیدا کرده و پاسخ دهد. لطفا توجه داشته باشید که حجم فایل نباید از 5 مگابایت بیشتر باشد. docx. xlsx. .txt xls.pdf فرمت‌های مجاز شامل هستند. ', + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Icon(Icons.file_upload_outlined, + size: 32, color: AppColors.gray[700]), + ), + Text( + 'پسوندهای مجاز: pdf، xls، xlsx، docx، txt\nحداکثر حجم فایل‌: 5 مگابایت\nحداکثر تعداد فایل‌: 3 عدد', + style: AppTextStyles.body4 + .copyWith(color: AppColors.gray[700]), + textDirection: TextDirection.rtl, + textAlign: TextAlign.justify, + ), + const SizedBox( + height: 8, + ), + LoadingButton( + onPressed: () async { + if (selectedFiles.value.length >= 5) return; + List? files = [...selectedFiles.value]; + final fs = await PickFileService(context) + .getFile( + fileType: FileType.custom, + allowMultiple: true, + allowedExtensions: [ + 'pdf', + 'docx', + 'xls', + 'xlsx', + 'txt' + ], + maxSize: 5); + + if (fs != null) { + files.addAll(fs); + + if (files.length > 5) { + files = files.sublist(0, 5); + } + selectedFiles.value = [...files]; + } + }, + color: AppColors.primaryColor.defaultShade, + radius: 360, + child: Text('افزودن فایل', + style: AppTextStyles.body3 + .copyWith(color: Colors.white))), + ValueListenableBuilder( + valueListenable: selectedFiles, + builder: (context, files, _) { + return ListView.builder( + physics: + const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: files.length, + itemBuilder: (context, index) { + final file = files[index]; + + return Container( + margin: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(10), + color: context + .read() + .isDark() + ? AppColors.black[900] + : AppColors.primaryColor[50]), + child: Directionality( + textDirection: + file.name.startsWithPersian() + ? TextDirection.rtl + : TextDirection.ltr, + child: ListTile( + title: Text(file.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 16, + color: AppColors + .green.defaultShade)), + subtitle: FutureBuilder( + future: file.length(), + builder: (context, size) { + return Text( + '${file.name.split('.').last}: ${size.hasData && size.data != null ? '${(size.data! / 1048576).toStringAsFixed(2)} MB' : ''}'); + }), + leading: GestureDetector( + onTap: () { + selectedFiles.value = List.from( + selectedFiles.value) + ..removeAt(index); + }, + child: Assets.icon.outline.trash + .svg(width: 24, height: 24), + ), + ), + ), + ); + }); + }) + ], + )), + labeledCard( + label: 'لینک‌های مرتبط ', + hint: + 'در صورتی که دستیار شما باید از منابع آنلاین برای ارائه پاسخ‌ها استفاده کند، می‌توانید. لینک‌های مربوطه را در این بخش وارد کنید. به عنوان مثال اگر دستیار شما باید از یک وب سایت خاص برای جمع آوری داده‌ها استفاده کند، لینک آن سایت را اینجا قرار دهید. این لینک‌ها به عنوان منابع مرجع برای پاسخگویی به سوالات استفاده خواهند شد. برای اضافه کردن لینک از دکمه + استفاده کنید.', + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Icon(Icons.link, + size: 32, color: AppColors.gray[700]), + ), + Padding( + padding: const EdgeInsets.all(16), + child: FilledTextField( + stateController: linkFormState, + hintText: linkFormState.hintText, + onValid: (value) { + if (value != null) { + if (!value.isURL()) { + return 'لینک نا معتبر است'; + } + } + return null; + }, + ), + ), + LoadingButton( + onPressed: () { + if (!linkFormState.formState.currentState! + .validate()) { + return; + } + if (!linkFormState.formController.text + .isURL()) { + return; + } + links.value = [ + ...links.value, + linkFormState.formController.text + ]; + linkFormState.formController.clear(); + }, + color: AppColors.primaryColor.defaultShade, + radius: 360, + child: Text('تأیید', + style: AppTextStyles.body3 + .copyWith(color: Colors.white))), + ValueListenableBuilder( + valueListenable: links, + builder: (context, ls, _) { + return ListView.builder( + physics: + const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: ls.length, + itemBuilder: (context, index) { + final link = ls[index]; + return Container( + margin: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(10), + color: context + .read() + .isDark() + ? AppColors.black[900] + : AppColors.primaryColor[50]), + child: ListTile( + title: Text(link, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textDirection: TextDirection.ltr, + style: AppTextStyles.body4 + .copyWith( + color: AppColors + .green.defaultShade)), + leading: GestureDetector( + onTap: () { + links.value = + List.from(links.value) + ..removeAt(index); + }, + child: Assets.icon.outline.trash + .svg(width: 24, height: 24), + ), + ), + ); + }); + }) + ], + )), + Container( + margin: const EdgeInsets.symmetric(vertical: 16.0), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: context.read().isDark() + ? Theme.of(context).colorScheme.surface + : AppColors.green[50].withValues(alpha: 0.2)), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ValueListenableBuilder( + valueListenable: isPublic, + builder: (context, enable, _) { + return Transform.scale( + scale: 0.8, + child: Switch.adaptive( + value: enable, + onChanged: (value) { + isPublic.value = value; + }, + ), + ); + }), + Text( + 'نمایش عمومی دستیار', + style: AppTextStyles.body3.copyWith( + fontWeight: FontWeight.bold, + color: context.read().isDark() + ? Theme.of(context).colorScheme.onSurface + : AppColors.primaryColor.defaultShade), + ) + ], + ), + const SizedBox( + height: 12, + ), + Directionality( + textDirection: TextDirection.rtl, + child: Text( + 'اگر این گزینه رو فعال کنی، دستیار هوش مصنوعی که ساختی برای سایر کاربران هم قابل مشاهده و استفاده می‌شه! ', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + ) + ], + ), + ), + + LoadingButton( + loading: state is CreateAssistantLoading, + onPressed: () { + if (context.read().state + is DeleteAssistantLoading) { + return; + } + + // botIdError.value = false; + categoryIdError.value = false; + final List valids = []; + valids.add( + nameFormState.formState.currentState!.validate()); + valids.add(descriptionFormState.formState.currentState! + .validate()); + valids.add( + promptFormState.formState.currentState!.validate()); + if (!checkReqs() && valids.contains(false)) { + SnackBarManager(context, id: 'fill-form').show( + message: + 'مقادیر خواسته شده را به درستی وارد کنید!', + status: SnackBarStatus.error); + return; + } + context.read().add( + CreateAnAssistant( + id: widget.info?.id, + model: CreateAssistantRequestModel( + // botId: botId!, + categoryId: categoryId!, + name: nameFormState.formController.text, + description: descriptionFormState + .formController.text, + prompt: promptFormState.formController.text, + public: isPublic.value, + // image: selectedImageFile.value!, + files: selectedFiles.value, + links: links.value.isEmpty + ? null + : links.value))); + }, + color: AppColors.primaryColor.defaultShade, + radius: 360, + height: 48, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + widget.info != null + ? 'تایید و ویرایش' + : 'تایید و ساخت', + style: AppTextStyles.body3 + .copyWith(color: Colors.white), + ), + ), + const SizedBox( + width: 4, + ), + const Icon( + CupertinoIcons.check_mark, + color: Colors.white, + size: 18, + ) + ], + )), + if (widget.info != null) + Column( + children: [ + const SizedBox( + height: 8, + ), + BlocConsumer( + listener: (context, state) { + if (state is DeleteAssistantSuccess) { + context + .read() + .add(GetAll()); + context.pop(); + } + if (state is DeleteAssistantFail) { + SnackBarManager(context, + id: 'createAssistantError') + .show( + status: SnackBarStatus.error, + message: + state.message ?? 'خطا از طرف سرور'); + } + }, + builder: (context, state) { + return LoadingButton( + loading: state is DeleteAssistantLoading, + onPressed: () async { + DialogHandler(context: context) + .showDeleteItem( + title: 'دستیار حذف شود؟', + description: + 'با این کار اطلاعات شما ازبین خواهد رفت.', + onConfirm: () async { + context + .read() + .delete(widget.info!.id!); + }, + ); + }, + color: Theme.of(context).colorScheme.secondary, + radius: 360, + isOutlined: true, + height: 48, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + 'حذف دستیار', + style: AppTextStyles.body3.copyWith( + color: Theme.of(context) + .colorScheme + .secondary), + ), + ), + const SizedBox( + width: 4, + ), + Icon( + CupertinoIcons.delete, + color: Theme.of(context) + .colorScheme + .secondary, + size: 18, + ) + ], + )); + }, + ), + ], + ), + const SizedBox( + height: 16, + ), + ], + ), + ), + ), + ), + ), + ); + } + + Stack labeledCard( + {required final String label, + required final Widget child, + final String? error, + final String? hint}) { + return Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + width: MediaQuery.sizeOf(context).width, + margin: const EdgeInsets.symmetric(vertical: 16.0), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: error != null + ? AppColors.red.defaultShade + : AppColors.gray[700])), + child: child), + if (error != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + error, + style: AppTextStyles.body4.copyWith( + color: AppColors.red.defaultShade, + fontWeight: FontWeight.bold), + ), + ), + ], + ), + Positioned( + top: 0, + right: 24, + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, + padding: const EdgeInsets.symmetric(horizontal: 8), + child: hint == null + ? Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + label, + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + ) + : titleWithHint( + text: label, + hint: hint, + textStyle: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface)))) + ], + ); + } + + Widget titleWithHint( + {required final String text, + final String? hint, + final TextStyle? textStyle}) { + return Tooltip( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: context.read().isDark() + ? AppColors.gray[900] + : AppColors.primaryColor[50], + borderRadius: BorderRadius.circular(8)), + triggerMode: TooltipTriggerMode.tap, + enableTapToDismiss: true, + enableFeedback: true, + preferBelow: true, + showDuration: const Duration(minutes: 2), + richMessage: WidgetSpan( + alignment: PlaceholderAlignment.baseline, + baseline: TextBaseline.alphabetic, + child: Container( + padding: const EdgeInsets.all(10), + constraints: const BoxConstraints(maxWidth: 300), + child: Text( + hint ?? '', + style: AppTextStyles.body4 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + textDirection: TextDirection.rtl, + textAlign: TextAlign.justify, + ), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + text, + style: textStyle ?? + AppTextStyles.body4 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ), + ), + if (hint != null) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + width: 4, + ), + Assets.icon.outline.warning2.svg( + color: context.read().isDark() + ? Colors.white + : AppColors.primaryColor.defaultShade) + ], + ) + ], + ), + ); + } +} diff --git a/lib/ui/screens/main/assistant/cubit/delete_assistant_cubit.dart b/lib/ui/screens/main/assistant/cubit/delete_assistant_cubit.dart new file mode 100644 index 0000000..6ede9e3 --- /dev/null +++ b/lib/ui/screens/main/assistant/cubit/delete_assistant_cubit.dart @@ -0,0 +1,24 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/repository/bot_repository.dart'; + +part 'delete_assistant_state.dart'; + +class DeleteAssistantCubit extends Cubit { + DeleteAssistantCubit() : super(DeleteAssistantInitial()); + + Future delete(int id) async { + emit(DeleteAssistantLoading()); + try { + await BotRepository.deleteBot(id: id); + emit(DeleteAssistantSuccess()); + } on DioException catch (e) { + emit(DeleteAssistantFail(message: e.response?.data['detail'])); + if (kDebugMode) { + print("Dio Error is: $e"); + } + } + } +} diff --git a/lib/ui/screens/main/assistant/cubit/delete_assistant_state.dart b/lib/ui/screens/main/assistant/cubit/delete_assistant_state.dart new file mode 100644 index 0000000..e743d8f --- /dev/null +++ b/lib/ui/screens/main/assistant/cubit/delete_assistant_state.dart @@ -0,0 +1,20 @@ +part of 'delete_assistant_cubit.dart'; + +sealed class DeleteAssistantState extends Equatable { + const DeleteAssistantState(); + + @override + List get props => []; +} + +final class DeleteAssistantInitial extends DeleteAssistantState {} + +final class DeleteAssistantLoading extends DeleteAssistantState {} + +final class DeleteAssistantFail extends DeleteAssistantState { + final String? message; + + const DeleteAssistantFail({required this.message}); +} + +final class DeleteAssistantSuccess extends DeleteAssistantState {} diff --git a/lib/ui/screens/main/assistant/cubit/personal_assistant_info_cubit.dart b/lib/ui/screens/main/assistant/cubit/personal_assistant_info_cubit.dart new file mode 100644 index 0000000..b46aac3 --- /dev/null +++ b/lib/ui/screens/main/assistant/cubit/personal_assistant_info_cubit.dart @@ -0,0 +1,25 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/assistant_personal_info_model.dart'; +import 'package:hoshan/data/repository/bot_repository.dart'; + +part 'personal_assistant_info_state.dart'; + +class PersonalAssistantInfoCubit extends Cubit { + PersonalAssistantInfoCubit() : super(PersonalAssistantInfoInitial()); + + Future getInfo(int id) async { + emit(PersonalAssistantInfoLoading()); + try { + final response = await BotRepository.getAssistantPersonalInfo(id); + emit(PersonalAssistantInfoSucess(info: response.assistantPersonalInfo!)); + } on DioException catch (e) { + emit(PersonalAssistantInfoFail()); + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } +} diff --git a/lib/ui/screens/main/assistant/cubit/personal_assistant_info_state.dart b/lib/ui/screens/main/assistant/cubit/personal_assistant_info_state.dart new file mode 100644 index 0000000..7e25198 --- /dev/null +++ b/lib/ui/screens/main/assistant/cubit/personal_assistant_info_state.dart @@ -0,0 +1,20 @@ +part of 'personal_assistant_info_cubit.dart'; + +sealed class PersonalAssistantInfoState extends Equatable { + const PersonalAssistantInfoState(); + + @override + List get props => []; +} + +final class PersonalAssistantInfoInitial extends PersonalAssistantInfoState {} + +final class PersonalAssistantInfoLoading extends PersonalAssistantInfoState {} + +final class PersonalAssistantInfoSucess extends PersonalAssistantInfoState { + final AssistantPersonalInfo info; + + const PersonalAssistantInfoSucess({required this.info}); +} + +final class PersonalAssistantInfoFail extends PersonalAssistantInfoState {} diff --git a/lib/ui/screens/main/assistant/global_assistants_screen.dart b/lib/ui/screens/main/assistant/global_assistants_screen.dart new file mode 100644 index 0000000..5875ccf --- /dev/null +++ b/lib/ui/screens/main/assistant/global_assistants_screen.dart @@ -0,0 +1,365 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/data/model/empty_states_enum.dart'; +import 'package:hoshan/data/model/tools_categories_model.dart'; +import 'package:hoshan/ui/screens/main/assistant/bloc/global_assistants_bloc.dart'; +import 'package:hoshan/ui/screens/main/forum/cubit/category_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/bot/bot_grid_card.dart'; +import 'package:hoshan/ui/widgets/components/bot/bot_grid_card_placeholder.dart'; +import 'package:hoshan/ui/widgets/sections/empty/empty_states.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; + +class GlobalAssistantsScreen extends StatefulWidget { + const GlobalAssistantsScreen({super.key}); + + @override + State createState() => _GlobalAssistantsScreenState(); +} + +ValueNotifier initiaGlobalCatItem = + ValueNotifier(Categories(id: -1, name: 'برگزیده‌ها')); + +class _GlobalAssistantsScreenState extends State { + void onSelect(BuildContext contxet, Bots bot) { + context.go(Routes.assistant, extra: bot.id); + } + + void onMark(bool marked, Bots bot, int index) { + final newBot = bot; + // if (initialItem.value.id == -2) { + // context.read().add(RemoveGlobalAssistantBot( + // oldAssistantsIndex: index, + // oldBot: newBot, + // )); + // return; + // } + newBot.marked = marked; + context.read().add(ChangeGlobalAssistantBot( + oldAssistantsIndex: index, oldBot: bot, newBot: newBot)); + } + + final ScrollController scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + backgroundColor: Theme.of(context).colorScheme.surface, + color: Theme.of(context).colorScheme.primary, + onRefresh: () async { + context + .read() + .add(GetGlobalAssistants(category: initiaGlobalCatItem.value.id)); + scrollController.jumpTo(0); + }, + child: Column( + children: [ + BlocBuilder( + builder: (context, state) { + if (state is CategoryLoading) { + return SizedBox( + height: 36, + child: ListView.builder( + padding: + const EdgeInsetsDirectional.symmetric(horizontal: 16), + itemCount: 10, + physics: const NeverScrollableScrollPhysics(), + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + return categoryPlaceholder(); + }, + ), + ); + } + if (state is CategorySuccess) { + final categories = [ + Categories(id: -1, name: 'برگزیده‌ها'), + Categories(id: -2, name: 'علاقه‌مندی‌ها'), + ...state.categories + ]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Directionality( + textDirection: TextDirection.rtl, + child: SizedBox( + height: 36, + width: MediaQuery.sizeOf(context).width, + child: ListView.builder( + padding: const EdgeInsetsDirectional.symmetric( + horizontal: 16), + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + itemCount: categories.length, + itemBuilder: (context, index) { + final cat = categories[index]; + return GestureDetector( + onTap: () { + if (context.read().state + is GlobalAssistantsLoading) { + return; + } + if (initiaGlobalCatItem.value.id == cat.id) { + return; + } + context.read().add( + GetGlobalAssistants(category: cat.id)); + initiaGlobalCatItem.value = cat; + }, + child: categoryContainer(cat)); + }, + )), + ), + ); + } + return const SizedBox.shrink(); + }, + ), + Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state is GlobalAssistantsFail) { + return EmptyStates.getEmptyState( + status: EmptyStatesEnum.server, scale: 0.8); + } + if (state is GlobalAssistantsSuccess) { + final assistants = state.assistants; + if (assistants.isEmpty) { + if (initiaGlobalCatItem.value.id == -2) { + return Center( + child: Column( + children: [ + const SizedBox( + height: 32, + ), + Assets.icon.gif.emptyBookmarks.image(), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + 'اینجا دستیارهای نشان‌دار شده شما نمایش داده می‌شوند. هنوز موردی اضافه نکرده‌اید. برای دسترسی سریع‌تر، دستیارهای دلخواهتان را بوکمارک کنید!', + style: AppTextStyles.body4.copyWith( + color: + Theme.of(context).colorScheme.onSurface), + textDirection: TextDirection.rtl, + textAlign: TextAlign.center, + ), + ) + ], + )); + } + + return EmptyStates.getEmptyState( + status: EmptyStatesEnum.assistant, + title: 'دستیاری وجود ندارد', + height: MediaQuery.sizeOf(context).height * 0.4, + ); + } + return Directionality( + textDirection: TextDirection.rtl, + child: ListView.builder( + itemCount: assistants.length, + shrinkWrap: true, + padding: EdgeInsets.only(bottom: 90), + physics: const BouncingScrollPhysics(), + itemBuilder: (context, index) { + final assistant = assistants[index]; + return state.category == null || + state.category == -1 || + state.category == -2 + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Builder(builder: (context) { + return ListTile( + title: Text( + assistant.categoryName ?? '', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + ); + }), + rowsList(assistant.bots!) + ], + ) + : gridsList(assistant.bots!); + }, + ), + ); + } + return Directionality( + textDirection: TextDirection.rtl, + child: ListView.builder( + itemCount: 10, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + child: DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16)), + child: Text( + 'عمومی', + style: AppTextStyles.body4 + .copyWith(color: AppColors.black[900]), + ), + ), + ), + ), + rowsListPlaceHolder() + ], + ); + }, + ), + ); + }, + ), + ), + ], + ), + ); + } + + Widget gridsList(List bots) { + return GridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: Responsive(context).isMobile() + ? 2 + : Responsive(context).isTablet() + ? 3 + : 5, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + mainAxisExtent: 150), + itemCount: bots.length, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, index) { + return GestureDetector( + onTap: () => onSelect(context, bots[index]), + child: BotGridCard( + onMark: (marked) => onMark(marked, bots[index], index), + bot: bots[index], + ), + ); + }, + ); + } + + Widget rowsList(List bots) { + return SizedBox( + height: 170, + child: ListView.builder( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 8), + itemCount: bots.length, + itemBuilder: (context, index) { + return SizedBox( + width: 180, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8), + child: GestureDetector( + onTap: () => onSelect(context, bots[index]), + child: BotGridCard( + onMark: (marked) => onMark(marked, bots[index], index), + bot: bots[index], + ), + ), + )); + }, + ), + ); + } + + Widget rowsListPlaceHolder() { + return SizedBox( + height: 170, + child: ListView.builder( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: 10, + itemBuilder: (context, index) { + return SizedBox( + width: 180, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8), + child: BotGridCardPlaceholder( + index: index, + ), + )); + }, + ), + ); + } + + Widget categoryContainer(Categories cat) { + return ValueListenableBuilder( + valueListenable: initiaGlobalCatItem, + builder: (context, item, child) { + final active = item.id == cat.id; + return Container( + padding: const EdgeInsets.all(0), + margin: const EdgeInsets.symmetric(horizontal: 4), + constraints: const BoxConstraints(minWidth: 100), + alignment: Alignment.center, + decoration: BoxDecoration( + color: active + ? AppColors.primaryColor[ + context.read().isDark() ? 500 : 50] + : AppColors.gray[ + context.read().isDark() ? 900 : 400], + borderRadius: BorderRadius.circular(360)), + child: Text( + cat.name ?? '', + style: AppTextStyles.body4.copyWith( + color: context.read().isDark() + ? Colors.white + : active + ? AppColors.primaryColor.defaultShade + : AppColors.black[300]), + ), + ); + }); + } + + Widget categoryPlaceholder() { + return DefaultPlaceHolder( + child: Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.symmetric(horizontal: 4), + constraints: const BoxConstraints(minWidth: 100), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.white, borderRadius: BorderRadius.circular(360)), + child: Text( + 'for placeholder', + style: AppTextStyles.body4, + ), + ), + ); + } +} diff --git a/lib/ui/screens/main/assistant/personal_assistants_screen.dart b/lib/ui/screens/main/assistant/personal_assistants_screen.dart new file mode 100644 index 0000000..9900670 --- /dev/null +++ b/lib/ui/screens/main/assistant/personal_assistants_screen.dart @@ -0,0 +1,688 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/data/model/empty_states_enum.dart'; +import 'package:hoshan/data/model/personal_assistants_bots.dart'; +import 'package:hoshan/ui/screens/main/assistant/bloc/personal_assistants_bloc.dart'; +import 'package:hoshan/ui/screens/main/assistant/cubit/personal_assistant_info_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:hoshan/ui/widgets/sections/empty/empty_states.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; + +class PersonalAssistantsScreen extends StatefulWidget { + const PersonalAssistantsScreen({super.key}); + + @override + State createState() => + _PersonalAssistantsScreenState(); +} + +class _PersonalAssistantsScreenState extends State { + final ScrollController scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + backgroundColor: Theme.of(context).colorScheme.surface, + color: Theme.of(context).colorScheme.primary, + onRefresh: () async { + context.read().add(GetAll()); + scrollController.jumpTo(0); + }, + child: SingleChildScrollView( + controller: scrollController, + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics()), + child: Responsive(context).maxWidthInDesktop( + maxWidth: 800, + child: (contxet, mw) => + BlocBuilder( + builder: (context, state) { + if (state is PersonalAssistantsSuccess) { + return ListView.builder( + itemCount: state.personalAssistants.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + final personalAssistant = state.personalAssistants[index]; + + return Column( + children: [ + GestureDetector( + onTap: () { + if (personalAssistant.status == 'confirmed') { + context.go(Routes.assistant, + extra: personalAssistant.id); + } + }, + child: + personalAssistantContainer(personalAssistant)), + if (state is PersonalAssistantInfoLoading) + Container( + color: Colors.white.withValues(alpha: 0.5), + child: Center( + child: SpinKitThreeBounce( + size: 32, + color: Theme.of(context).colorScheme.primary, + ), + ), + ) + ], + ); + }, + ); + } + if (state is PersonalAssistantsEmpty || + state is PersonalAssistantsFail) { + return EmptyStates.getEmptyState( + status: EmptyStatesEnum.assistant, + title: 'هنوز دستیاری توسط شما ساخته نشده‌', + ); + } + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: 10, + itemBuilder: (context, index) => + personalAssistantContainerPlaceholder(context), + ); + }, + ), + ), + ), + ); + } + + Widget personalAssistantContainer(PersonalAssistant personalAssistant) { + return BlocProvider( + create: (context) => PersonalAssistantInfoCubit(), + child: + BlocConsumer( + listener: (context, state) { + if (state is PersonalAssistantInfoSucess) { + context.go(Routes.createAssistant, extra: state.info); + } + }, + builder: (context, state) { + return Directionality( + textDirection: TextDirection.rtl, + child: Column( + children: [ + Container( + padding: + const EdgeInsets.symmetric(horizontal: 32, vertical: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ImageNetwork( + baseUrl: DioService.baseURL, + url: personalAssistant.image, + width: 50, + height: 50, + radius: 360, + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + personalAssistant.name ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface), + ), + const SizedBox( + height: 4, + ), + Text( + personalAssistant.description ?? '', + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900]), + ), + const SizedBox( + height: 8, + ), + personalAssistant.status == 'confirmed' + ? Column( + children: [ + Row( + children: [ + Expanded( + flex: 4, + child: Row( + children: [ + Text( + 'تاییدشده', + style: AppTextStyles.body4.copyWith( + color: AppColors + .gray[context + .read< + ThemeModeCubit>() + .isDark() + ? 600 + : 900]), + ), + const SizedBox( + width: 2, + ), + Assets.icon.bold.verify.svg( + width: 16, + height: 16, + color: AppColors + .green.defaultShade) + ], + ), + ), + const Expanded( + flex: 1, child: SizedBox()), + Expanded( + flex: 4, + child: Row( + textDirection: TextDirection.ltr, + children: [ + if (personalAssistant.comments != + null) + Row( + children: [ + Text( + personalAssistant.comments + .toString(), + style: AppTextStyles.body4 + .copyWith( + color: AppColors + .gray[context + .read< + ThemeModeCubit>() + .isDark() + ? 600 + : 900]), + ), + const SizedBox( + width: 4, + ), + Assets.icon.outline.messages + .svg( + width: 18, + height: 18, + color: Theme.of( + context) + .colorScheme + .primary), + ], + ), + // Row( + // children: [ + // Padding( + // padding: + // const EdgeInsets.only(top: 2.0), + // child: Text( + // '12', + // style: AppTextStyles.body4.copyWith( + // color: AppColors.gray[context + // .read() + // .isDark() + // ? 600 + // :900]), + // ), + // ), + // Icon( + // Icons.remove_red_eye_outlined, + // size: 20, + // color: AppColors + // .primaryColor.defaultShade, + // ), + // ], + // ), + if (personalAssistant.score != + null) + Row( + children: [ + Padding( + padding: + const EdgeInsets.only( + top: 4.0), + child: Text( + personalAssistant.score + .toString(), + style: AppTextStyles + .body4 + .copyWith( + color: AppColors + .gray[context + .read< + ThemeModeCubit>() + .isDark() + ? 600 + : 900]), + ), + ), + const SizedBox( + width: 4, + ), + Icon( + Icons.star_border_rounded, + size: 24, + color: Theme.of(context) + .colorScheme + .primary, + ), + const SizedBox( + width: 16, + ), + ], + ), + ], + ), + ) + ], + ), + const SizedBox( + height: 8, + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + LoadingButton( + loading: state + is PersonalAssistantInfoLoading, + onPressed: () { + context + .read< + PersonalAssistantInfoCubit>() + .getInfo(personalAssistant.id!); + }, + isOutlined: true, + color: Theme.of(context) + .colorScheme + .primary, + backgroundColor: Theme.of(context) + .scaffoldBackgroundColor, + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + 'ویرایش', + style: AppTextStyles.body4 + .copyWith( + color: Theme.of(context) + .colorScheme + .primary), + ), + Assets.icon.outline.edit2.svg( + color: Theme.of(context) + .colorScheme + .primary, + width: 18, + height: 18, + ) + ], + ), + ), + ], + ) + ], + ) + : personalAssistant.status == 'rejected' + ? Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + 'تاییدنشده', + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[context + .read< + ThemeModeCubit>() + .isDark() + ? 600 + : 900]), + ), + const SizedBox( + width: 2, + ), + Assets.icon.bold.verify.svg( + width: 16, + height: 16, + color: + AppColors.red.defaultShade) + ], + ), + LoadingButton( + loading: state + is PersonalAssistantInfoLoading, + onPressed: () { + context + .read< + PersonalAssistantInfoCubit>() + .getInfo( + personalAssistant.id!); + }, + color: AppColors.red[50], + child: Text( + 'بازبینی', + style: AppTextStyles.body4 + .copyWith( + color: AppColors + .red.defaultShade), + )) + ], + ) + : personalAssistant.status == 'pending' + ? Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + 'در انتظار تایید', + style: AppTextStyles.body4 + .copyWith( + color: AppColors + .gray[context + .read< + ThemeModeCubit>() + .isDark() + ? 600 + : 900]), + ), + const SizedBox( + width: 4, + ), + SpinKitCircle( + size: 24, + color: Theme.of(context) + .colorScheme + .primary, + ) + ], + ), + LoadingButton( + loading: state + is PersonalAssistantInfoLoading, + onPressed: () { + context + .read< + PersonalAssistantInfoCubit>() + .getInfo( + personalAssistant.id!); + }, + isOutlined: true, + color: Theme.of(context) + .colorScheme + .primary, + backgroundColor: Theme.of(context) + .scaffoldBackgroundColor, + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + 'ویرایش', + style: AppTextStyles.body4 + .copyWith( + color: Theme.of( + context) + .colorScheme + .primary), + ), + Assets.icon.outline.edit2.svg( + color: Theme.of(context) + .colorScheme + .primary, + width: 18, + height: 18, + ) + ], + ), + ), + ], + ) + : const SizedBox.shrink() + ], + )) + ], + ), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Divider(), + ) + ], + ), + ); + }, + ), + ); + } + + Directionality personalAssistantContainerPlaceholder(BuildContext context) { + return Directionality( + textDirection: TextDirection.rtl, + child: Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultPlaceHolder( + child: Container( + width: 50, + height: 50, + decoration: const BoxDecoration( + shape: BoxShape.circle, color: Colors.white), + )), + const SizedBox( + width: 12, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Text( + 'ساخت پاورپوینت', + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SizedBox( + height: 4, + ), + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Text( + 'با وارد کردن محتوای دلخواهتان، یک پاورپوینت حرفه‌ای و آماده ارائه ایجاد کنید.', + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900]), + ), + ), + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + Expanded( + flex: 4, + child: DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Row( + children: [ + Text( + 'تاییدشده', + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[context + .read() + .isDark() + ? 600 + : 900]), + ), + const SizedBox( + width: 2, + ), + Assets.icon.bold.verify.svg( + width: 16, + height: 16, + color: AppColors.green.defaultShade) + ], + ), + ), + ), + ), + const Expanded(flex: 1, child: SizedBox()), + Expanded( + flex: 4, + child: DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + '12', + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[context + .read() + .isDark() + ? 600 + : 900]), + ), + Assets.icon.outline.messages.svg( + width: 18, + height: 18, + color: Theme.of(context) + .colorScheme + .primary) + ], + ), + Row( + children: [ + Padding( + padding: + const EdgeInsets.only(top: 2.0), + child: Text( + '12', + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[context + .read() + .isDark() + ? 600 + : 900]), + ), + ), + Icon( + Icons.remove_red_eye_outlined, + size: 20, + color: Theme.of(context) + .colorScheme + .primary, + ), + ], + ), + Row( + children: [ + Padding( + padding: + const EdgeInsets.only(top: 2.0), + child: Text( + '12', + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[context + .read() + .isDark() + ? 600 + : 900]), + ), + ), + Icon( + Icons.star_border_rounded, + size: 20, + color: Theme.of(context) + .colorScheme + .primary, + ), + ], + ), + ], + ), + ), + ), + ) + ], + ), + const SizedBox( + height: 8, + ), + DefaultPlaceHolder( + child: Container( + width: MediaQuery.sizeOf(context).width, + height: 48, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + )) + ], + )) + ], + ), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Divider(), + ) + ], + ), + ); + } +} diff --git a/lib/ui/screens/main/forum/cubit/category_cubit.dart b/lib/ui/screens/main/forum/cubit/category_cubit.dart new file mode 100644 index 0000000..5fa7881 --- /dev/null +++ b/lib/ui/screens/main/forum/cubit/category_cubit.dart @@ -0,0 +1,29 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/tools_categories_model.dart'; +import 'package:hoshan/data/repository/bot_repository.dart'; + +part 'category_state.dart'; + +class CategoryCubit extends Cubit { + CategoryCubit() : super(CategoryInitial()); + + void getAllCategorie() async { + emit(CategoryLoading()); + try { + final response = await BotRepository.getAllCategories(); + if (response.categories == null || response.categories!.isEmpty) { + emit(CategoryEmpty()); + } else { + emit(CategorySuccess(categories: response.categories!)); + } + } on DioException catch (e) { + emit(CategoryFail()); + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } +} diff --git a/lib/ui/screens/main/forum/cubit/category_state.dart b/lib/ui/screens/main/forum/cubit/category_state.dart new file mode 100644 index 0000000..33d2220 --- /dev/null +++ b/lib/ui/screens/main/forum/cubit/category_state.dart @@ -0,0 +1,22 @@ +part of 'category_cubit.dart'; + +sealed class CategoryState extends Equatable { + final List categories; + + const CategoryState({this.categories = const []}); + + @override + List get props => [categories]; +} + +final class CategoryInitial extends CategoryState {} + +final class CategoryLoading extends CategoryState {} + +final class CategorySuccess extends CategoryState { + const CategorySuccess({required super.categories}); +} + +final class CategoryFail extends CategoryState {} + +final class CategoryEmpty extends CategoryState {} diff --git a/lib/ui/screens/main/forum/cubit/comment_like_cubit.dart b/lib/ui/screens/main/forum/cubit/comment_like_cubit.dart new file mode 100644 index 0000000..4ae4357 --- /dev/null +++ b/lib/ui/screens/main/forum/cubit/comment_like_cubit.dart @@ -0,0 +1,56 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/repository/forum_repository.dart'; + +part 'comment_like_state.dart'; + +class CommentLikeCubit extends Cubit { + CommentLikeCubit() : super(const CommentLikeInitial()); + + void getLike(int? like) { + switch (like) { + case 0: + emit(const CommentDisLiked()); + + break; + case 1: + emit(const CommentLiked()); + + break; + default: + emit(const CommentLikeInitial()); + } + } + + void setLiked({required final int id, required int status}) async { + final oldState = state; + emit(CommentLikeLoading()); + try { + await ForumRepository.likedMessage(id: id, status: status); + + switch (status) { + case 0: + emit(CommentDisLiked( + disLike: 1, like: oldState is CommentLikeInitial ? 0 : -1)); + + break; + case 1: + emit(CommentLiked( + like: 1, disLike: oldState is CommentLikeInitial ? 0 : -1)); + + break; + default: + emit(CommentLikeInitial( + like: oldState is CommentLiked ? -1 : 0, + disLike: oldState is CommentDisLiked ? -1 : 0)); + } + } on DioException catch (e) { + emit(oldState); + if (kDebugMode) { + print("Dio Error: $e"); + } + } + } +} diff --git a/lib/ui/screens/main/forum/cubit/comment_like_state.dart b/lib/ui/screens/main/forum/cubit/comment_like_state.dart new file mode 100644 index 0000000..452cc4a --- /dev/null +++ b/lib/ui/screens/main/forum/cubit/comment_like_state.dart @@ -0,0 +1,24 @@ +part of 'comment_like_cubit.dart'; + +sealed class CommentLikeState extends Equatable { + final int? like; + final int? disLike; + const CommentLikeState({this.like, this.disLike}); + + @override + List get props => []; +} + +final class CommentLikeInitial extends CommentLikeState { + const CommentLikeInitial({super.like, super.disLike}); +} + +final class CommentLikeLoading extends CommentLikeState {} + +final class CommentLiked extends CommentLikeState { + const CommentLiked({super.like, super.disLike}); +} + +final class CommentDisLiked extends CommentLikeState { + const CommentDisLiked({super.like, super.disLike}); +} diff --git a/lib/ui/screens/main/forum/cubit/comments_cubit.dart b/lib/ui/screens/main/forum/cubit/comments_cubit.dart new file mode 100644 index 0000000..5ae270c --- /dev/null +++ b/lib/ui/screens/main/forum/cubit/comments_cubit.dart @@ -0,0 +1,86 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/forum_model.dart'; +import 'package:hoshan/data/model/sort_by_model.dart'; +import 'package:hoshan/data/repository/forum_repository.dart'; + +part 'comments_state.dart'; + +class CommentsCubit extends Cubit { + CommentsCubit() : super(CommentsInitial()); + + static int page = 1; + static int categoriesId = 1; + static SortByModel sortByModel = SortByModel(text: 'جدیدترین', value: 'date'); + static int? lastPage; + static final List comments = []; + + void loadComments( + {required final int cId, + final bool retry = false, + final SortByModel? orderBy}) async { + if (cId != categoriesId || retry) { + emit(CommentsInitial()); + page = 1; + lastPage = null; + categoriesId = cId; + comments.clear(); + } else if (lastPage != null && page > lastPage!) { + return; + } else if (page != 1) { + emit(CommentsLoading(comments: comments)); + } + + try { + if (orderBy != null) sortByModel = orderBy; + + final cModel = await ForumRepository.getForumComments( + categoryId: categoriesId, page: page, orderBy: sortByModel.value); + + page++; + lastPage = cModel.lastPage; + comments.addAll(cModel.comments!); + + emit(CommentsSuccess(comments: comments)); + } on DioException catch (e) { + emit(CommentsFail()); + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } + + void addComment({required final Comment comment}) { + emit(CommentsLoading(comments: comments)); + comments.insert(0, comment); + emit(CommentsSuccess(comments: comments)); + } + + void changeComment({ + required final Comment newComment, + }) { + emit(CommentsLoading(comments: comments)); + final index = comments.indexWhere( + (element) => element.id == newComment.id, + ); + comments[index] = newComment; + emit(CommentsSuccess(comments: comments)); + } + + void addReplies({required final int commentId}) { + emit(CommentsLoading(comments: comments)); + final comment = comments.firstWhere( + (element) => element.id == commentId, + ); + final index = comments.indexOf(comment); + if (comment.replies == null) { + comment.replies = 1; + } else { + comment.replies = comment.replies! + 1; + } + comments[index] = comment; + emit(CommentsSuccess(comments: comments)); + } +} diff --git a/lib/ui/screens/main/forum/cubit/comments_state.dart b/lib/ui/screens/main/forum/cubit/comments_state.dart new file mode 100644 index 0000000..45f3cb3 --- /dev/null +++ b/lib/ui/screens/main/forum/cubit/comments_state.dart @@ -0,0 +1,21 @@ +part of 'comments_cubit.dart'; + +sealed class CommentsState extends Equatable { + final List comments; + + const CommentsState({this.comments = const []}); + @override + List get props => [comments]; +} + +final class CommentsInitial extends CommentsState {} + +final class CommentsLoading extends CommentsState { + const CommentsLoading({super.comments}); +} + +final class CommentsSuccess extends CommentsState { + const CommentsSuccess({super.comments}); +} + +final class CommentsFail extends CommentsState {} diff --git a/lib/ui/screens/main/forum/cubit/replies_cubit.dart b/lib/ui/screens/main/forum/cubit/replies_cubit.dart new file mode 100644 index 0000000..04c0667 --- /dev/null +++ b/lib/ui/screens/main/forum/cubit/replies_cubit.dart @@ -0,0 +1,62 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/forum_model.dart'; +import 'package:hoshan/data/repository/forum_repository.dart'; +import 'package:hoshan/ui/screens/main/forum/cubit/comments_cubit.dart'; + +part 'replies_state.dart'; + +class RepliesCubit extends Cubit { + RepliesCubit() : super(RepliesInitial()); + + void loadReplies({required final Comment comment}) async { + int? lastPage = state.lastPage; + int page = state.page; + if (lastPage != null && page > lastPage) return; + emit( + RepliesLoading(replies: state.replies, lastPage: lastPage, page: page)); + try { + final cModel = await ForumRepository.getForumCommentsReplies( + id: comment.id!, categoryId: CommentsCubit.categoriesId, page: page); + + page++; + lastPage = cModel.lastPage; + final updatedList = List.from(state.replies); + updatedList.addAll(cModel.replies!); + + emit( + RepliesSuccess(replies: updatedList, lastPage: lastPage, page: page)); + } on DioException catch (e) { + emit(RepliesFail()); + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } + + void clear() { + emit(const RepliesSuccess(replies: [], lastPage: null, page: 1)); + } + + void addReply( + {required final Comment comment, required final Comment parent}) { + emit(RepliesLoading( + replies: state.replies, lastPage: state.lastPage, page: state.page)); + final updatedList = List.from(state.replies); + if (updatedList.contains(parent)) { + final index = updatedList.indexOf(parent); + if (updatedList[index] == updatedList.last) { + updatedList.add(comment); + } else { + updatedList.insert(index + 1, comment); + } + } else { + updatedList.insert(0, comment); + } + + emit(RepliesSuccess( + replies: updatedList, lastPage: state.lastPage, page: state.page)); + } +} diff --git a/lib/ui/screens/main/forum/cubit/replies_state.dart b/lib/ui/screens/main/forum/cubit/replies_state.dart new file mode 100644 index 0000000..be4ecc5 --- /dev/null +++ b/lib/ui/screens/main/forum/cubit/replies_state.dart @@ -0,0 +1,25 @@ +part of 'replies_cubit.dart'; + +sealed class RepliesState extends Equatable { + final List replies; + final int page; + final int? lastPage; + + const RepliesState({this.replies = const [], this.lastPage, this.page = 1}); + @override + List get props => [replies, page, lastPage ?? 0]; +} + +final class RepliesInitial extends RepliesState {} + +final class RepliesLoading extends RepliesState { + const RepliesLoading( + {required super.replies, required super.lastPage, required super.page}); +} + +final class RepliesSuccess extends RepliesState { + const RepliesSuccess( + {required super.replies, required super.lastPage, required super.page}); +} + +final class RepliesFail extends RepliesState {} diff --git a/lib/ui/screens/main/forum/forum_screen.dart b/lib/ui/screens/main/forum/forum_screen.dart new file mode 100644 index 0000000..4ddde35 --- /dev/null +++ b/lib/ui/screens/main/forum/forum_screen.dart @@ -0,0 +1,1086 @@ +// ignore_for_file: deprecated_member_use_from_same_package, use_build_context_synchronously + +import 'package:cross_file/cross_file.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/core/utils/date_time.dart'; +import 'package:hoshan/core/utils/strings.dart'; +import 'package:hoshan/data/model/empty_states_enum.dart'; +import 'package:hoshan/data/model/forum_model.dart'; +import 'package:hoshan/data/model/sort_by_model.dart'; +import 'package:hoshan/data/model/tools_categories_model.dart'; +import 'package:hoshan/data/repository/forum_repository.dart'; +import 'package:hoshan/ui/screens/main/forum/cubit/category_cubit.dart'; +import 'package:hoshan/ui/screens/main/forum/cubit/comments_cubit.dart'; +import 'package:hoshan/ui/screens/main/forum/cubit/replies_cubit.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/dialog/bottom_sheets.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:hoshan/ui/widgets/sections/empty/empty_states.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; + +class ForumScreen extends StatefulWidget { + const ForumScreen({super.key}); + + @override + State createState() => _ForumScreenState(); +} + +class _ForumScreenState extends State + with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + final ScrollController scrollController = ScrollController(); + + void loadCommentsFromCategory(int id) { + if (id == CommentsCubit.categoriesId) return; + if (context.read().state is CommentsInitial) return; + context.read().loadComments(cId: id, retry: true); + } + + void loadCommentsFromSortBy(SortByModel sortBy) { + context.read().loadComments( + cId: CommentsCubit.categoriesId, retry: true, orderBy: sortBy); + } + + Future sendComment({required String message, XFile? file}) async { + try { + final comment = await ForumRepository.sendForum( + text: message, + categoryId: CommentsCubit.categoriesId, + image: file, + ); + if (mounted) { + final userInfo = UserInfoCubit.userInfoModel; + final user = User( + id: userInfo.id, + image: userInfo.image, + name: userInfo.name, + username: userInfo.username); + + context + .read() + .addComment(comment: comment.copyWith(user: user)); + + context.pop(); + } + } on DioException catch (e) { + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } + + Future sendReply( + {required BuildContext context, + required String message, + required int parentId, + required String repliedUserId, + XFile? file}) async {} + + @override + void initState() { + super.initState(); + + scrollController.addListener( + () { + if (scrollController.position.pixels == + scrollController.position.maxScrollExtent && + context.read().state is CommentsSuccess) { + context + .read() + .loadComments(cId: CommentsCubit.categoriesId); + } + }, + ); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + floatingActionButton: FloatingActionButton.small( + heroTag: 'add-comment-fb', + onPressed: () async { + await BottomSheetHandler(context).showAddComment( + onSend: (message, file) async => + sendComment(message: message, file: file), + ); + }, + shape: const CircleBorder(), + backgroundColor: AppColors.primaryColor.defaultShade, + child: const Icon( + CupertinoIcons.add, + color: Colors.white, + ), + ), + body: RefreshIndicator( + backgroundColor: Theme.of(context).colorScheme.surface, + color: Theme.of(context).colorScheme.primary, + onRefresh: () async { + // context.read().getAllCategorie(); + context + .read() + .loadComments(cId: CommentsCubit.categoriesId, retry: true); + scrollController.jumpTo(0); + }, + child: SingleChildScrollView( + controller: scrollController, + physics: context.watch().state is CommentsInitial + ? const NeverScrollableScrollPhysics() + : const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics()), + child: Column( + children: [ + BlocBuilder( + builder: (context, state) { + if (state is CategoryLoading) { + return categoriesPlaceholder(); + } + if (state is CategorySuccess) { + final categories = state.categories; + List oddCategories = []; + List evenCategories = []; + for (int i = 0; i < categories.length; i++) { + if (i % 2 == 0) { + evenCategories.add(categories[i]); + } else { + oddCategories.add(categories[i]); + } + } + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Directionality( + textDirection: TextDirection.rtl, + child: Stack( + children: [ + SizedBox( + width: MediaQuery.sizeOf(context).width, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: + const EdgeInsets.fromLTRB(120, 0, 16, 0), + physics: const BouncingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (evenCategories.isNotEmpty) + SizedBox( + height: 36, + child: Row( + children: [ + ...List.generate( + evenCategories.length, + (index) { + final cat = + evenCategories[index]; + return categoryContainer(cat); + }, + ) + ], + ), + ), + const SizedBox( + height: 8, + ), + if (oddCategories.isNotEmpty) + SizedBox( + height: 36, + child: Row( + children: [ + const SizedBox( + width: 16, + ), + ...List.generate( + oddCategories.length, + (index) { + final cat = + oddCategories[index]; + return categoryContainer(cat); + }, + ) + ], + ), + ), + ], + ), + ), + ), + if (context.watch().state + is CommentsSuccess || + context.watch().state + is CommentsFail) + Positioned( + left: 0, + bottom: 0, + top: 0, + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 4), + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + blurRadius: 16, + color: Theme.of(context) + .scaffoldBackgroundColor, + spreadRadius: 10) + ], + color: Theme.of(context) + .scaffoldBackgroundColor, + gradient: RadialGradient(colors: [ + Theme.of(context) + .scaffoldBackgroundColor, + Theme.of(context) + .scaffoldBackgroundColor + .withValues(alpha: 0.7), + Theme.of(context) + .scaffoldBackgroundColor + .withValues(alpha: 0.6), + ])), + child: GestureDetector( + onTap: () => + BottomSheetHandler(context).showSortBy( + initailValue: CommentsCubit.sortByModel, + items: [ + SortByModel( + text: 'جدیدترین', value: 'date'), + SortByModel( + text: 'پربحث‌ترین‌ها', + value: 'replies'), + ], + onSelected: (item) => + loadCommentsFromSortBy(item), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16), + child: Icon( + CupertinoIcons.sort_down, + color: Theme.of(context) + .colorScheme + .onSurface, + )), + ), + ), + ) + ], + ), + ), + ); + } + return const SizedBox.shrink(); + }, + ), + const SizedBox( + height: 12, + ), + BlocBuilder( + builder: (context, state) { + if (state is CommentsInitial) { + return ListView.builder( + itemCount: 20, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return commentContainerPlaceholder(); + }, + ); + } + + if (state.comments.isEmpty) { + return Center( + child: EmptyStates.getEmptyState( + status: EmptyStatesEnum.messages)); + } + + return Column( + children: [ + ListView.builder( + itemCount: state.comments.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + final comment = state.comments[index]; + + return BlocProvider( + create: (context) => RepliesCubit(), + child: Builder(builder: (context) { + return Column( + children: [ + commentContainer(context, comment), + Padding( + padding: const EdgeInsets.only(right: 24), + child: + BlocBuilder( + builder: (context, state) { + return Column( + children: [ + if (state.replies.isNotEmpty) + ListView.builder( + physics: + const NeverScrollableScrollPhysics(), + itemCount: state.replies.length, + shrinkWrap: true, + itemBuilder: (context, index) { + final reply = + state.replies[index]; + return commentContainer( + context, + reply, + parrentComment: comment, + ); + }, + ), + BlocBuilder( + builder: (context, state) { + return Column( + children: [ + if (state.lastPage != + null && + state.page > + state.lastPage!) + Padding( + padding: + const EdgeInsets + .symmetric( + vertical: 12.0), + child: GestureDetector( + onTap: () { + if (comment.replies != + null && + comment.replies! > + 0) { + if (state.lastPage != + null && + state.page > + state + .lastPage!) { + final lastPosition = + scrollController + .position; + context + .read< + RepliesCubit>() + .clear(); + + scrollController.jumpTo(scrollController + .position + .pixels - + lastPosition + .pixels); + } else { + context + .read< + RepliesCubit>() + .loadReplies( + comment: + comment); + } + } + }, + child: Row( + children: [ + const Expanded( + flex: 6, + child: + Divider()), + if (comment.replies != + null && + comment.replies! > + 0) + Padding( + padding: const EdgeInsets + .symmetric( + horizontal: + 8.0), + child: Text( + 'پنهان کردن پاسخ‌ها', + style: AppTextStyles.body4.copyWith( + color: AppColors + .gray[context + .read() + .isDark() + ? 600 + : 900]), + ), + ), + const Expanded( + child: + Divider()), + ], + ), + ), + ), + ], + ); + }, + ), + ], + ); + }, + ), + ) + ], + ); + }), + ); + }, + ), + if (state is CommentsLoading) + Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: LinearProgressIndicator( + color: AppColors.primaryColor.defaultShade, + borderRadius: BorderRadius.circular(8), + ), + ), + const SizedBox( + height: 36, + ) + ], + ); + }, + ) + ], + ), + ), + ), + ); + } + + GestureDetector categoryContainer(Categories cat) { + return GestureDetector( + onTap: () => loadCommentsFromCategory(cat.id!), + child: Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.symmetric(horizontal: 4), + constraints: const BoxConstraints(minWidth: 100), + alignment: Alignment.center, + decoration: BoxDecoration( + color: CommentsCubit.categoriesId == cat.id + ? AppColors.primaryColor[ + context.read().isDark() ? 500 : 50] + : AppColors + .gray[context.read().isDark() ? 900 : 400], + borderRadius: BorderRadius.circular(360)), + child: Text( + cat.name ?? '', + style: AppTextStyles.body4.copyWith( + color: context.read().isDark() + ? Colors.white + : CommentsCubit.categoriesId == cat.id + ? AppColors.primaryColor.defaultShade + : AppColors.black[300]), + ), + ), + ); + } + + Widget commentContainer(BuildContext context, Comment comment, + {final Comment? parrentComment}) { + final daysAgo = DateTimeUtils.getDaysBetweenNowAnd(comment.createdAt!); + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: parrentComment != null + ? Theme.of(context).colorScheme.surface + : context.read().isDark() + ? AppColors.black[900] + : AppColors.primaryColor[50], + borderRadius: BorderRadius.circular(16)), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + comment.createdAt != null + ? Row( + children: [ + Assets.icon.outline.clock.svg( + width: 20, + height: 20, + color: AppColors.gray[context + .read() + .isDark() + ? 600 + : 900]), + const SizedBox( + width: 8, + ), + Text( + daysAgo == 0 + ? 'امروز' + : ('$daysAgo روز پیش'), + style: AppTextStyles.body5.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + textDirection: TextDirection.rtl, + ) + ], + ) + : const SizedBox.shrink(), + Row( + children: [ + Text( + comment.user?.username ?? '', + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context) + .colorScheme + .onSurface), + ), + const SizedBox( + width: 8, + ), + ], + ) + ], + ), + const SizedBox( + height: 8, + ), + SizedBox( + width: MediaQuery.sizeOf(context).width, + child: Text( + comment.text ?? '', + style: AppTextStyles.body3.copyWith( + color: Theme.of(context).colorScheme.onSurface), + textDirection: comment.text != null && + comment.text!.startsWithEnglish() + ? TextDirection.ltr + : TextDirection.rtl, + ), + ), + const SizedBox( + height: 8, + ), + if (comment.image != null) + Column( + children: [ + Container( + margin: const EdgeInsets.symmetric(vertical: 12), + constraints: BoxConstraints( + maxHeight: + MediaQuery.sizeOf(context).height * 0.2), + alignment: Alignment.centerRight, + child: AspectRatio( + aspectRatio: 16 / 9, + child: ImageNetwork( + url: DioService.baseURL + comment.image!, + radius: 16, + showHero: true, + ), + ), + ), + const SizedBox( + height: 8, + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + GestureDetector( + onTap: () { + BottomSheetHandler(context) + .showReportOptions(); + }, + child: commentBtn( + icon: Assets.icon.outline.flag2, + text: 'گزارش')), + const SizedBox( + width: 12, + ), + // BlocProvider( + // create: (context) => + // CommentLikeCubit()..getLike(comment.userFeedback), + // child: BlocConsumer( + // listener: (context, state) { + // int likes = comment.likes!; + // int disLikes = comment.dislikes!; + // if (state.like != null) { + // likes = likes + state.like!; + // } + // if (state.disLike != null) { + // disLikes = disLikes + state.disLike!; + // } + // context.read().changeComment( + // newComment: comment.copyWith( + // likes: likes, + // dislikes: disLikes, + // userFeedback: state is CommentLiked + // ? 1 + // : state is CommentDisLiked + // ? 0 + // : -1)); + // }, + // builder: (context, state) { + // return Row( + // children: [ + // DefaultPlaceHolder( + // enabled: state is CommentLikeLoading, + // child: GestureDetector( + // onTap: () => context + // .read() + // .setLiked( + // id: comment.id!, + // status: + // state is CommentLiked ? -1 : 1), + // child: commentBtn( + // color: state is CommentLiked + // ? AppColors.green.defaultShade + // : null, + // icon: state is CommentLiked + // ? Assets.icon.bold.like + // : Assets.icon.outline.like, + // text: comment.likes?.toString() ?? '0'), + // ), + // ), + // const SizedBox( + // width: 12, + // ), + // DefaultPlaceHolder( + // enabled: state is CommentLikeLoading, + // child: GestureDetector( + // onTap: () => context + // .read() + // .setLiked( + // id: comment.id!, + // status: state is CommentDisLiked + // ? -1 + // : 0), + // child: commentBtn( + // color: state is CommentDisLiked + // ? AppColors.red.defaultShade + // : null, + // icon: state is CommentDisLiked + // ? Assets.icon.bold.dislike + // : Assets.icon.outline.dislike, + // text: + // comment.dislikes?.toString() ?? '0'), + // ), + // ), + // ], + // ); + // }, + // ), + // ), + // const SizedBox( + // width: 12, + // ), + if (comment.replies != null && + parrentComment == null && + comment.replies != 0) + commentBtn( + icon: Assets.icon.outline.messageText, + text: comment.replies!.toString()), + ], + ), + GestureDetector( + onTap: () async { + await BottomSheetHandler(context).showAddComment( + comment: comment, + onSend: (message, file) async { + try { + final commentRes = + await ForumRepository.sendForum( + text: message, + categoryId: + CommentsCubit.categoriesId, + image: file, + parentId: parrentComment?.id ?? + comment.id, + repliedUserId: + parrentComment?.user!.id ?? + comment.user!.id); + if (mounted) { + final userInfo = + UserInfoCubit.userInfoModel; + final user = User( + id: userInfo.id, + image: userInfo.image, + name: userInfo.name, + username: userInfo.username); + context.read().addReply( + parent: comment, + comment: + commentRes.copyWith(user: user)); + context.read().addReplies( + commentId: parrentComment?.id! ?? + comment.id!); + + context.pop(); + } + } on DioException catch (e) { + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + }, + ); + }, + child: Text('پاسخ دادن', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.primary, + )), + ) + ], + ), + ], + ), + ), + const SizedBox( + width: 12, + ), + ImageNetwork( + url: comment.user != null && comment.user!.image != null + ? DioService.baseURL + comment.user!.image! + : 'https://placehold.co/600x400', + width: 40, + height: 40, + radius: 360, + ) + ], + ), + ), + if (comment.replies != null && comment.replies! > 0) + BlocBuilder( + builder: (context, state) { + if (state.lastPage != null && state.page > state.lastPage!) { + return const SizedBox(); + } + return Column( + children: [ + state is RepliesLoading + ? Padding( + padding: const EdgeInsets.only(top: 24.0), + child: SpinKitThreeBounce( + color: AppColors.primaryColor.defaultShade, + size: 32, + ), + ) + : Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: GestureDetector( + onTap: () { + if (comment.replies != null && + comment.replies! > 0) { + if (state.lastPage != null && + state.page > state.lastPage!) { + final lastPosition = + scrollController.position; + context.read().clear(); + + scrollController.jumpTo( + scrollController.position.pixels - + lastPosition.pixels); + } else { + context + .read() + .loadReplies(comment: comment); + } + } + }, + child: Row( + children: [ + const Expanded(flex: 6, child: Divider()), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0), + child: Text( + 'مشاهده پاسخ‌ها', + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[context + .read() + .isDark() + ? 600 + : 900]), + ), + ), + const Expanded(child: Divider()), + ], + ), + ), + ), + ], + ); + }, + ), + ], + ), + ); + } + + Padding categoriesPlaceholder() { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Directionality( + textDirection: TextDirection.rtl, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + child: Column( + children: [ + SizedBox( + height: 36, + child: Row( + children: [ + ...List.generate( + 20, + (index) { + return DefaultPlaceHolder( + child: Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.symmetric(horizontal: 4), + constraints: const BoxConstraints(minWidth: 100), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(360)), + child: Text('$index'), + ), + ); + }, + ) + ], + ), + ), + const SizedBox( + height: 8, + ), + SizedBox( + height: 36, + child: Row( + children: [ + const SizedBox( + width: 24, + ), + ...List.generate( + 20, + (index) { + return DefaultPlaceHolder( + child: Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.symmetric(horizontal: 4), + constraints: const BoxConstraints(minWidth: 100), + alignment: Alignment.center, + decoration: BoxDecoration( + color: AppColors.gray[400], + borderRadius: BorderRadius.circular(360)), + child: Text('$index'), + ), + ); + }, + ) + ], + ), + ), + ], + ), + ), + ), + ); + } + + Container commentContainerPlaceholder() { + return Container( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + DefaultPlaceHolder( + child: Assets.icon.outline.clock.svg( + width: 20, + height: 20, + color: AppColors.gray[900]), + ), + const SizedBox( + width: 8, + ), + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Colors.white, + ), + child: Text( + 'time ago', + style: AppTextStyles.body5, + ), + ), + ) + ], + ), + Row( + children: [ + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Colors.white, + ), + child: Text( + 'this is a username', + style: AppTextStyles.body4 + .copyWith(fontWeight: FontWeight.bold), + ), + ), + ), + const SizedBox( + width: 8, + ), + ], + ) + ], + ), + const SizedBox( + height: 8, + ), + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Colors.white, + ), + child: Text( + 'بات‌های تولید عکس می‌تونه تصاویری با سبک‌های مختلف مثل نقاشی، سه‌بعدی یا رئال تولید کنه؟🎨🎨🎨', + style: AppTextStyles.body3, + textDirection: TextDirection.rtl, + ), + ), + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + Row( + children: [ + DefaultPlaceHolder( + child: commentBtn( + icon: Assets.icon.outline.flag2, + text: 'گزارش'), + ), + const SizedBox( + width: 12, + ), + DefaultPlaceHolder( + child: commentBtn( + icon: Assets.icon.outline.like, text: '---'), + ), + const SizedBox( + width: 12, + ), + DefaultPlaceHolder( + child: commentBtn( + icon: Assets.icon.outline.dislike, + text: '---'), + ), + const SizedBox( + width: 12, + ), + DefaultPlaceHolder( + child: commentBtn( + icon: Assets.icon.outline.messageText, + text: '---'), + ), + ], + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + DefaultPlaceHolder( + child: Text('پاسخ دادن', + style: AppTextStyles.body4.copyWith( + color: AppColors.primaryColor.defaultShade, + )), + ) + ], + ) + ], + ), + ), + const SizedBox( + width: 12, + ), + const DefaultPlaceHolder( + child: ImageNetwork( + url: 'https://placehold.co/600x400', + width: 40, + height: 40, + radius: 360, + ), + ) + ], + ), + ], + ), + ); + } + + Row commentBtn( + {required final SvgGenImage icon, + final String? text, + final Color? color}) { + return Row( + children: [ + icon.svg( + width: 18, + height: 18, + color: color ?? + AppColors + .gray[context.read().isDark() ? 600 : 900]), + if (text != null) + Row( + children: [ + const SizedBox( + width: 4, + ), + Text( + text, + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[ + context.read().isDark() ? 600 : 900]), + ), + ], + ) + ], + ); + } +} diff --git a/lib/ui/screens/main/generate_media_screen.dart b/lib/ui/screens/main/generate_media_screen.dart new file mode 100644 index 0000000..5b58cbe --- /dev/null +++ b/lib/ui/screens/main/generate_media_screen.dart @@ -0,0 +1,275 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/data/model/media_model.dart'; +import 'package:hoshan/ui/screens/gmedia/cubit/medias_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; + +class GenerateMediaScreen extends StatefulWidget { + const GenerateMediaScreen({super.key}); + + @override + State createState() => _GenerateMediaScreenState(); +} + +class _GenerateMediaScreenState extends State { + void onClick(Categories cat) { + String? route; + if (cat.icon!.contains('image')) { + route = Routes.generatPhoto; + } else if (cat.icon!.contains('audio')) { + route = Routes.generatAudio; + } else if (cat.icon!.contains('video')) { + route = Routes.generatVideo; + } + if (route != null) { + context.go('$route?id=${cat.id}'); + } + } + + @override + Widget build(BuildContext context) { + return Responsive(context).maxWidthInDesktop( + child: (contxet, mw) => Padding( + padding: const EdgeInsets.all(8.0), + child: BlocBuilder( + builder: (context, state) { + if (state is MediasFail) { + return const SizedBox.shrink(); + } + if (state is MediasSuccess) { + final medias = state.medias; + + return Column(children: [ + // AiBanner(), + Expanded( + flex: 2, + child: genCard( + onTap: () { + onClick(medias.categories!.first); + }, + title: medias.categories!.first.name!, + photoUrl: medias.categories!.first.image!, + color: switch (medias.categories!.first.name) { + 'تولید عکس' => Colors.amber, + 'تولید صدا' => Colors.purple, + 'تولید ویدیو' => Colors.blue, + _ => Colors.indigo, + })), + Expanded( + flex: 2, + child: Row( + children: List.generate( + medias.categories!.length - 1, + (index) { + final media = medias.categories![index + 1]; + return Expanded( + child: genCard( + onTap: () { + onClick(media); + }, + title: media.name!, + photoUrl: media.image!, + color: switch (media.name) { + 'تولید عکس' => Colors.amber, + 'تولید صدا' => Colors.purple, + 'تولید ویدیو' => Colors.blue, + _ => Colors.indigo, + })); + }, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: InkWell( + onTap: () { + context.go(Routes.cmp); + }, + child: Container( + width: MediaQuery.sizeOf(context).width, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + AppColors.secondryColor[600].withAlpha(40), + AppColors.secondryColor[600].withAlpha(140), + AppColors.secondryColor.defaultShade, + ]), + borderRadius: BorderRadius.circular(16)), + alignment: Alignment.centerRight, + child: Stack( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'دوئل خلاقیت', + style: AppTextStyles.headline5 + .copyWith(color: Colors.white), + ), + Text( + 'مجموعه مسابقات هوشان', + style: AppTextStyles.body4 + .copyWith(color: Colors.white), + ), + ], + ), + ), + ], + ), + Align( + alignment: const Alignment(-0.5, 0), + child: Assets.icon.gif.medal.image( + height: double.infinity, fit: BoxFit.fill)) + ], + ), + ), + ), + ), + ), + ]); + } + return const Center( + child: CircularProgressIndicator(), + ); + }, + ), + ), + ); + } + + Widget genCard( + {required final String photoUrl, + required final String title, + required final MaterialColor color, + final Function()? onTap}) { + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Stack( + children: [ + ImageNetwork( + radius: 16, + width: double.infinity, + height: double.infinity, + url: photoUrl), + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + color.shade600.withAlpha(20), + color.shade600.withAlpha(100), + color, + ]), + borderRadius: BorderRadius.circular(16)), + alignment: Alignment.bottomRight, + padding: const EdgeInsets.all(16), + child: Text( + title, + style: AppTextStyles.headline6.copyWith(color: Colors.white), + ), + ), + ) + ], + ), + ), + ); + } + + Container generateCard(BuildContext context, + {required final SvgGenImage icon, + required final SvgGenImage banner, + required final String title, + final bool inverse = false}) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Theme.of(context).colorScheme.primary), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Theme.of(context).colorScheme.surface, + context.read().isDark() + ? AppColors.primaryColor[800] + : AppColors.primaryColor[50], + ])), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + textDirection: inverse ? TextDirection.rtl : TextDirection.ltr, + children: [ + const SizedBox( + width: 8, + ), + Expanded( + flex: 2, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + height: 16, + ), + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Theme.of(context).colorScheme.onSurface)), + width: 80, + height: 80, + child: Center( + child: icon.svg( + width: 36, + height: 36, + color: Theme.of(context).colorScheme.primary)), + ), + const SizedBox( + height: 16, + ), + Container( + width: double.infinity, + padding: + const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + title, + style: AppTextStyles.body4.copyWith( + color: Colors.white, fontWeight: FontWeight.bold), + )), + ) + ], + ), + ), + ), + Expanded(flex: 3, child: banner.svg(fit: BoxFit.contain)), + ], + ), + ); + } +} diff --git a/lib/ui/screens/main/home/bloc/bots_bloc.dart b/lib/ui/screens/main/home/bloc/bots_bloc.dart new file mode 100644 index 0000000..63bc960 --- /dev/null +++ b/lib/ui/screens/main/home/bloc/bots_bloc.dart @@ -0,0 +1,54 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/data/repository/bot_repository.dart'; + +part 'bots_event.dart'; +part 'bots_state.dart'; + +class BotsBloc extends Bloc { + static List allBots = []; + static List createBots = []; + BotsBloc() : super(BotsInitial()) { + on((event, emit) async { + if (event is GetAllBots) { + emit(BotsLoading()); + try { + final response = await BotRepository.getBots(); + final List publicBots = []; + final List privateBots = []; + final List toolBots = []; + if (response.bots != null) { + createBots.clear(); + allBots.clear(); + allBots.addAll(response.bots!); + for (var bot in response.bots!) { + if (bot.public != null) { + if (bot.tool ?? false) { + toolBots.add(bot); + } else { + createBots.add(bot); + } + if (bot.public!) { + publicBots.add(bot); + } else { + privateBots.add(bot); + } + } + } + } + emit(BotsSuccess( + publicBots: publicBots, + privateBots: privateBots, + toolBots: toolBots)); + } on DioException catch (e) { + emit(BotsFail()); + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } + }); + } +} diff --git a/lib/ui/screens/main/home/bloc/bots_event.dart b/lib/ui/screens/main/home/bloc/bots_event.dart new file mode 100644 index 0000000..e6b4ddb --- /dev/null +++ b/lib/ui/screens/main/home/bloc/bots_event.dart @@ -0,0 +1,7 @@ +part of 'bots_bloc.dart'; + +sealed class BotsEvent { + const BotsEvent(); +} + +class GetAllBots extends BotsEvent {} diff --git a/lib/ui/screens/main/home/bloc/bots_state.dart b/lib/ui/screens/main/home/bloc/bots_state.dart new file mode 100644 index 0000000..571f82e --- /dev/null +++ b/lib/ui/screens/main/home/bloc/bots_state.dart @@ -0,0 +1,20 @@ +part of 'bots_bloc.dart'; + +sealed class BotsState {} + +final class BotsInitial extends BotsState {} + +final class BotsLoading extends BotsState {} + +final class BotsSuccess extends BotsState { + final List publicBots; + final List privateBots; + final List toolBots; + + BotsSuccess( + {required this.publicBots, + required this.privateBots, + required this.toolBots}); +} + +final class BotsFail extends BotsState {} diff --git a/lib/ui/screens/main/home/bloc/courses_bloc.dart b/lib/ui/screens/main/home/bloc/courses_bloc.dart new file mode 100644 index 0000000..37e2ebc --- /dev/null +++ b/lib/ui/screens/main/home/bloc/courses_bloc.dart @@ -0,0 +1,43 @@ + +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/core/services/api/courses_services.dart'; +import 'package:hoshan/data/model/courses_model.dart'; + +part 'courses_event.dart'; +part 'courses_state.dart'; + +class CoursesBloc extends Bloc { + CoursesBloc() : super(CoursesInitial()) { + on((event, emit) async { + if (event is GetAllCourses) { + emit(CoursesLoading()); + try { + final courses = []; + + final response = await CoursesServices.dio.get( + CoursesServices.getCourses, + options: Options(headers: CoursesServices.getAuth())); + response.data.forEach((v) { + courses.add(Courses.fromJson(v)); + }); + courses.removeWhere( + (element) { + return element.categories + ?.any((category) => category.id == 139) ?? + false; + }, + ); + emit(CoursesSuccess(courses: courses)); + } on DioException catch (e) { + emit(CoursesFail()); + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } + }); + } +} diff --git a/lib/ui/screens/main/home/bloc/courses_event.dart b/lib/ui/screens/main/home/bloc/courses_event.dart new file mode 100644 index 0000000..93bdcd6 --- /dev/null +++ b/lib/ui/screens/main/home/bloc/courses_event.dart @@ -0,0 +1,10 @@ +part of 'courses_bloc.dart'; + +sealed class CoursesEvent extends Equatable { + const CoursesEvent(); + + @override + List get props => []; +} + +class GetAllCourses extends CoursesEvent {} diff --git a/lib/ui/screens/main/home/bloc/courses_state.dart b/lib/ui/screens/main/home/bloc/courses_state.dart new file mode 100644 index 0000000..71c1a1b --- /dev/null +++ b/lib/ui/screens/main/home/bloc/courses_state.dart @@ -0,0 +1,20 @@ +part of 'courses_bloc.dart'; + +sealed class CoursesState extends Equatable { + const CoursesState(); + + @override + List get props => []; +} + +final class CoursesInitial extends CoursesState {} + +final class CoursesLoading extends CoursesState {} + +final class CoursesSuccess extends CoursesState { + final List courses; + + const CoursesSuccess({required this.courses}); +} + +final class CoursesFail extends CoursesState {} diff --git a/lib/ui/screens/main/home/bloc/cubit/posts_cubit.dart b/lib/ui/screens/main/home/bloc/cubit/posts_cubit.dart new file mode 100644 index 0000000..bb69811 --- /dev/null +++ b/lib/ui/screens/main/home/bloc/cubit/posts_cubit.dart @@ -0,0 +1,59 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/core/services/api/courses_services.dart'; +import 'package:hoshan/data/model/posts_model.dart'; + +part 'posts_state.dart'; + +class PostsCubit extends Cubit { + PostsCubit() : super(PostsInitial()); + + void getLastPosts() async { + print('🔵 getLastPosts called'); + // Disabled enabled.json check - always show posts + // try { + // final check = await CoursesServices.dio.get('/enabled.json'); + // print('🟢 enabled.json: ${check.data}'); + // if (check.data == false) { + // emit(PostsFail()); + // return; + // } + // } catch (e) { + // // Ignore enabled.json check errors + // } + emit(PostsLoading()); + try { + final posts = []; + + final response = await CoursesServices.dio.get( + CoursesServices.getPosts, + ); + + print('🟢 API Response: ${response.statusCode}'); + print('🟢 Data type: ${response.data.runtimeType}'); + print('🟢 Data length: ${response.data?.length}'); + + response.data.forEach((v) { + posts.add(PostsModel.fromJson(v)); + }); + + if (kDebugMode) { + print('✅ Fetched ${posts.length} posts from WordPress'); + } + + emit(PostsSuccess(posts: posts)); + } on DioException catch (e) { + emit(PostsFail()); + if (kDebugMode) { + print('❌ Posts Error: ${e.message}'); + } + } catch (e) { + emit(PostsFail()); + if (kDebugMode) { + print('❌ Posts Error: $e'); + } + } + } +} diff --git a/lib/ui/screens/main/home/bloc/cubit/posts_state.dart b/lib/ui/screens/main/home/bloc/cubit/posts_state.dart new file mode 100644 index 0000000..5961786 --- /dev/null +++ b/lib/ui/screens/main/home/bloc/cubit/posts_state.dart @@ -0,0 +1,20 @@ +part of 'posts_cubit.dart'; + +sealed class PostsState extends Equatable { + const PostsState(); + + @override + List get props => []; +} + +final class PostsInitial extends PostsState {} + +final class PostsLoading extends PostsState {} + +final class PostsFail extends PostsState {} + +final class PostsSuccess extends PostsState { + final List posts; + + const PostsSuccess({required this.posts}); +} diff --git a/lib/ui/screens/main/home/cubit/banners_cubit.dart b/lib/ui/screens/main/home/cubit/banners_cubit.dart new file mode 100644 index 0000000..bf46bbf --- /dev/null +++ b/lib/ui/screens/main/home/cubit/banners_cubit.dart @@ -0,0 +1,25 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/banner_model.dart'; +import 'package:hoshan/data/repository/chatbot_repository.dart'; + +part 'banners_state.dart'; + +class BannersCubit extends Cubit { + BannersCubit() : super(BannersInitial()); + + void getBanners() async { + emit(BannersLoading()); + try { + final response = await ChatbotRepository.getBanners(); + emit(BannersSuccess(banners: response)); + } on DioException catch (e) { + emit(BannersFail()); + if (kDebugMode) { + print("Dio Error is: $e"); + } + } + } +} diff --git a/lib/ui/screens/main/home/cubit/banners_state.dart b/lib/ui/screens/main/home/cubit/banners_state.dart new file mode 100644 index 0000000..37b3e44 --- /dev/null +++ b/lib/ui/screens/main/home/cubit/banners_state.dart @@ -0,0 +1,20 @@ +part of 'banners_cubit.dart'; + +sealed class BannersState extends Equatable { + const BannersState(); + + @override + List get props => []; +} + +final class BannersInitial extends BannersState {} + +final class BannersLoading extends BannersState {} + +final class BannersSuccess extends BannersState { + final List banners; + + const BannersSuccess({required this.banners}); +} + +final class BannersFail extends BannersState {} diff --git a/lib/ui/screens/main/home/cubit/best_assistants_cubit.dart b/lib/ui/screens/main/home/cubit/best_assistants_cubit.dart new file mode 100644 index 0000000..31d6762 --- /dev/null +++ b/lib/ui/screens/main/home/cubit/best_assistants_cubit.dart @@ -0,0 +1,37 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/data/model/tools_categories_model.dart'; +import 'package:hoshan/data/repository/bot_repository.dart'; + +part 'best_assistants_state.dart'; + +class BestAssistantsCubit extends Cubit { + BestAssistantsCubit() : super(BestAssistantsInitial()); + + void getAssistants() async { + emit(BestAssistantsLoading()); + try { + final cats = await BotRepository.getGlobalAssistant(); + final List allBots = cats.categories!.expand((e) { + return e.bots!.take(2).map( + (b) { + return b.copyWith( + category: Categories(name: e.categoryName, id: b.category?.id)); + }, + ).toList(); + }).toList(); + allBots.sort((a, b) => (b.messages ?? 0).compareTo(a.messages ?? 0)); + emit(BestAssistantsSuccess( + assistants: allBots, + )); + } on DioException catch (e) { + emit(BestAssistantsFail()); + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } +} diff --git a/lib/ui/screens/main/home/cubit/best_assistants_state.dart b/lib/ui/screens/main/home/cubit/best_assistants_state.dart new file mode 100644 index 0000000..6d96d74 --- /dev/null +++ b/lib/ui/screens/main/home/cubit/best_assistants_state.dart @@ -0,0 +1,20 @@ +part of 'best_assistants_cubit.dart'; + +sealed class BestAssistantsState extends Equatable { + const BestAssistantsState(); + + @override + List get props => []; +} + +final class BestAssistantsInitial extends BestAssistantsState {} + +final class BestAssistantsLoading extends BestAssistantsState {} + +final class BestAssistantsFail extends BestAssistantsState {} + +final class BestAssistantsSuccess extends BestAssistantsState { + final List assistants; + + const BestAssistantsSuccess({required this.assistants}); +} diff --git a/lib/ui/screens/main/home/cubit/main_chat_bot_cubit.dart b/lib/ui/screens/main/home/cubit/main_chat_bot_cubit.dart new file mode 100644 index 0000000..5981668 --- /dev/null +++ b/lib/ui/screens/main/home/cubit/main_chat_bot_cubit.dart @@ -0,0 +1,25 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/data/repository/bot_repository.dart'; + +part 'main_chat_bot_state.dart'; + +class MainChatBotCubit extends Cubit { + MainChatBotCubit() : super(MainChatBotInitial()); + + void getBot(int id) async { + emit(MainChatBotLoading()); + try { + final bot = await BotRepository.getSingleBot(id: id); + emit(MainChatBotSuccess(bot: bot)); + } on DioException catch (e) { + emit(MainChatBotFail()); + if (kDebugMode) { + print('on Dio Error: $e'); + } + } + } +} diff --git a/lib/ui/screens/main/home/cubit/main_chat_bot_state.dart b/lib/ui/screens/main/home/cubit/main_chat_bot_state.dart new file mode 100644 index 0000000..42da0ea --- /dev/null +++ b/lib/ui/screens/main/home/cubit/main_chat_bot_state.dart @@ -0,0 +1,20 @@ +part of 'main_chat_bot_cubit.dart'; + +sealed class MainChatBotState extends Equatable { + const MainChatBotState(); + + @override + List get props => []; +} + +final class MainChatBotInitial extends MainChatBotState {} + +final class MainChatBotLoading extends MainChatBotState {} + +final class MainChatBotFail extends MainChatBotState {} + +final class MainChatBotSuccess extends MainChatBotState { + final Bots bot; + + const MainChatBotSuccess({required this.bot}); +} diff --git a/lib/ui/screens/main/home/home_screen.dart b/lib/ui/screens/main/home/home_screen.dart new file mode 100644 index 0000000..5c0740f --- /dev/null +++ b/lib/ui/screens/main/home/home_screen.dart @@ -0,0 +1,3293 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'dart:math'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/core/utils/strings.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/data/model/banner_model.dart'; +import 'package:hoshan/data/model/chat_args.dart'; +import 'package:hoshan/data/model/courses_model.dart'; +import 'package:hoshan/data/model/posts_model.dart'; +import 'package:hoshan/data/model/tools_categories_model.dart'; +import 'package:hoshan/ui/screens/gmedia/cubit/medias_cubit.dart'; +import 'package:hoshan/ui/screens/main/assistant/bloc/global_assistants_bloc.dart'; +import 'package:hoshan/ui/screens/main/assistant/global_assistants_screen.dart'; +import 'package:hoshan/ui/screens/main/home/bloc/bots_bloc.dart'; +import 'package:hoshan/ui/screens/main/home/bloc/courses_bloc.dart'; +import 'package:hoshan/ui/screens/main/home/bloc/cubit/posts_cubit.dart'; +import 'package:hoshan/ui/screens/main/home/cubit/banners_cubit.dart'; +import 'package:hoshan/ui/screens/main/home/cubit/best_assistants_cubit.dart'; +import 'package:hoshan/ui/screens/main/home/cubit/main_chat_bot_cubit.dart'; +import 'package:hoshan/ui/screens/main/home_page.dart'; +import 'package:hoshan/ui/screens/main/persons/cubit/persons_cubit.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/screens/tools/bloc/tools_bloc.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/ai_Banner.dart'; +import 'package:hoshan/ui/widgets/components/bot/bot_grid_card.dart'; +import 'package:hoshan/ui/widgets/components/bot/bot_grid_card_placeholder.dart'; +import 'package:hoshan/ui/widgets/components/bot/tool_card.dart'; +import 'package:hoshan/ui/widgets/components/bot/tool_card_placeholder.dart'; +import 'package:hoshan/ui/widgets/components/button/circle_icon_btn.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:hoshan/ui/widgets/components/slider/carousle_slider_banners.dart'; +import 'package:hoshan/ui/widgets/family_banner.dart'; + +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; +import 'package:string_validator/string_validator.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State + with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + final ScrollController scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + super.build(context); + return RefreshIndicator( + backgroundColor: Theme.of(context).colorScheme.surface, + color: Theme.of(context).colorScheme.primary, + onRefresh: () async { + context.read().getUserInfo(); + context.read().getAssistants(); + context.read().add(GetAllBots()); + context.read().add(GetAllTools()); + context.read().getBanners(); + context.read().getMedias(); + context.read().add(GetAllCourses()); + context.read().getLastPosts(); + context.read().getBot(1); + scrollController.jumpTo(0); + }, + child: SingleChildScrollView( + controller: scrollController, + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics()), + child: Directionality( + textDirection: TextDirection.rtl, + child: Column( + children: [ + FamilyBanner(), + + // carousleSliders(), + SizedBox( + height: 15, + ), + Row( + children: [ + Expanded( + child: Divider( + color: context.read().isDark() + ? Theme.of(context).colorScheme.onSurface + : AppColors.primaryColor[100])), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: context.read().isDark() + ? Theme.of(context).colorScheme.onSurface + : AppColors.primaryColor[100]), + child: Assets.icon.gif.chatMain.image( + width: 46, + ), + ), + Expanded( + child: Divider( + color: context.read().isDark() + ? Theme.of(context).colorScheme.onSurface + : AppColors.primaryColor[100])), + ], + ), + SizedBox( + height: 16, + ), + startChatSection(context), + Responsive(context).builder( + tabletAndMobileSame: true, + desktop: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: Column( + children: [ + const SizedBox( + height: 32, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Flexible(flex: 2, child: toolsList()), + Flexible( + child: Padding( + padding: const EdgeInsets.only(top: 32.0), + child: BlocBuilder( + builder: (context, state) { + if (state is BannersSuccess) { + if (state.banners.length < 3) { + return const SizedBox.shrink(); + } + Banners banner = + state.banners[state.banners.length - 2]; + + return bannerCard(context, banner); + } + if (state is BannersFail) { + return const SizedBox.shrink(); + } + return DefaultPlaceHolder( + child: Container( + height: Responsive(context).isMobile() + ? 80 + : 160, + width: double.infinity, + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.symmetric( + horizontal: 16.0) + .copyWith(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + ), + ); + }, + ), + ), + ) + ], + ), + const SizedBox( + height: 16, + ), + + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Flexible( + flex: 2, + child: BlocBuilder( + builder: (context, state) { + if (state is BestAssistantsFail) { + return const SizedBox.shrink(); + } + if (state is BestAssistantsSuccess) { + final List allBots = state.assistants; + allBots.shuffle(); + return Column( + children: [ + ListTile( + leading: Assets.icon.outline.assistant + .svg( + color: Theme.of(context) + .colorScheme + .onSurface, + width: 24, + height: 24), + title: Padding( + padding: + const EdgeInsets.only(top: 8.0), + child: Text( + 'دستیارهای برگزیده', + style: AppTextStyles.headline6 + .copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + ), + trailing: InkWell( + onTap: () { + screenIndex.value = 2; + initiaGlobalCatItem.value = + Categories( + id: -1, + name: 'برگزیده‌ها'); + context + .read() + .add( + const GetGlobalAssistants()); + }, + child: Text( + 'مشاهده همه', + style: AppTextStyles.body6 + .copyWith( + color: Theme.of(context) + .colorScheme + .primary, + fontWeight: + FontWeight.bold), + )), + ), + SizedBox( + width: double.infinity, + height: 170, + child: ListView.builder( + scrollDirection: Axis.horizontal, + physics: + const BouncingScrollPhysics(), + shrinkWrap: true, + itemCount: min(10, allBots.length), + padding: const EdgeInsets.symmetric( + horizontal: 8), + itemBuilder: (context, index) { + return SizedBox( + width: 160, + child: Padding( + padding: + const EdgeInsets.symmetric( + horizontal: 4.0, + vertical: 8), + child: InkWell( + onTap: () { + context.go( + Routes.assistant, + extra: allBots[index] + .id); + }, + child: BotGridCard( + showCat: true, + bot: allBots[index], + )), + ), + ); + }, + ), + ), + const SizedBox( + height: 16, + ), + ], + ); + } + return Column( + children: [ + DefaultPlaceHolder( + child: ListTile( + leading: Assets.icon.outline.assistant + .svg( + color: Theme.of(context) + .colorScheme + .onSurface, + width: 24, + height: 24), + title: Padding( + padding: + const EdgeInsets.only(top: 8.0), + child: Text( + 'دستیارهای برگزیده', + style: AppTextStyles.headline6 + .copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + ), + trailing: Text( + 'مشاهده همه', + style: AppTextStyles.body6.copyWith( + color: Theme.of(context) + .colorScheme + .primary, + fontWeight: FontWeight.bold), + ), + ), + ), + SizedBox( + width: double.infinity, + height: 170, + child: ListView.builder( + scrollDirection: Axis.horizontal, + physics: + const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: 10, + padding: const EdgeInsets.symmetric( + horizontal: 8), + itemBuilder: (context, index) { + return const SizedBox( + width: 160, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 4.0, vertical: 8), + child: BotGridCardPlaceholder(), + ), + ); + }, + ), + ), + const SizedBox( + height: 16, + ), + ], + ); + }, + ), + ), + Flexible( + child: Padding( + padding: const EdgeInsets.only(top: 32.0), + child: BlocBuilder( + builder: (context, state) { + if (state is BannersSuccess) { + if (state.banners.length < 3) { + return const SizedBox.shrink(); + } + Banners banner = state.banners.last; + return bannerCard(context, banner); + } + if (state is BannersFail) { + return const SizedBox.shrink(); + } + return DefaultPlaceHolder( + child: Container( + height: Responsive(context).isMobile() + ? 80 + : 160, + width: double.infinity, + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.symmetric( + horizontal: 16.0) + .copyWith(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + ), + ); + }, + ), + ), + ) + ], + ), + + BlocBuilder( + builder: (context, state) { + if (state is PersonsFail) { + return const SizedBox(); + } + if (state is PersonsSuccess) { + if (state.chars.categories?.isEmpty ?? true) { + return const SizedBox.shrink(); + } + final List bots = state.chars.categories! + .expand( + (genModel) => genModel.bots!.take(2)) + .toList() + ..shuffle(); + + return Column( + children: [ + ListTile( + leading: Assets.icon.outline.mageScanUser.svg( + color: Theme.of(context) + .colorScheme + .onSurface, + width: 24, + height: 24), + title: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + 'گفتگو با شخصیت‌ها', + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + ), + trailing: InkWell( + onTap: () { + screenIndex.value = 3; + }, + child: Text( + 'مشاهده همه', + style: AppTextStyles.body6.copyWith( + color: Theme.of(context) + .colorScheme + .primary, + fontWeight: FontWeight.bold), + )), + ), + SizedBox( + width: double.infinity, + height: 160, + child: ListView.builder( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + itemCount: bots.length, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric( + horizontal: 8), + itemBuilder: (context, index) { + final person = bots[index]; + return InkWell( + onTap: () { + context.go(Routes.chat, + extra: ChatArgs( + bot: person, isPerson: true)); + }, + child: Container( + margin: const EdgeInsets.all(8), + constraints: BoxConstraints( + maxWidth: + MediaQuery.sizeOf(context) + .width * + 0.2), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0x664D4D4D), + blurRadius: 6, + offset: Offset(0, 1), + spreadRadius: 0, + ) + ], + color: Theme.of(context) + .colorScheme + .surface, + borderRadius: + BorderRadius.circular(16)), + child: ClipRRect( + borderRadius: + BorderRadius.circular(16), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AspectRatio( + aspectRatio: 1 / 1, + child: ImageNetwork( + url: person.image ?? '', + fit: BoxFit.cover, + radius: 0, + height: double.infinity, + ), + ), + Expanded( + child: Padding( + padding: + const EdgeInsets.all( + 16.0), + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + mainAxisAlignment: + MainAxisAlignment + .center, + children: [ + Text( + person.name ?? '', + style: AppTextStyles + .body3 + .copyWith( + color: Theme.of( + context) + .colorScheme + .onSurface, + fontWeight: + FontWeight + .bold), + maxLines: 1, + overflow: TextOverflow + .ellipsis, + ), + Row( + children: [ + Assets.icon.outline.coin.svg( + width: 16, + height: 16, + color: Theme.of( + context) + .colorScheme + .secondary), + const SizedBox( + width: 4, + ), + Text( + person.cost == + 0 || + person.cost == + null + ? 'رایگان' + : person.cost + .toString(), + style: AppTextStyles.body5.copyWith( + color: Theme.of( + context) + .colorScheme + .secondary), + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + ) + ], + ), + Row( + children: [ + Container( + width: 8, + height: 8, + decoration: + BoxDecoration( + shape: BoxShape + .circle, + color: Theme.of( + context) + .colorScheme + .secondary, + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + person.category + ?.name ?? + '', + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + style: AppTextStyles + .body5 + .copyWith( + color: Theme.of( + context) + .colorScheme + .onSurface, + )), + ) + ], + ) + ], + ), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ], + ); + } + + return Column( + children: [ + ListTile( + leading: DefaultPlaceHolder( + child: Assets.icon.outline.mageScanUser.svg( + color: Theme.of(context) + .colorScheme + .onSurface, + width: 24, + height: 24), + ), + title: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: DefaultPlaceHolder( + child: Text( + 'گفتگو با شخصیت‌ها', + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + ), + ), + ), + SizedBox( + width: double.infinity, + height: 160, + child: ListView.builder( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + itemCount: 10, + physics: const NeverScrollableScrollPhysics(), + padding: + const EdgeInsets.symmetric(horizontal: 8), + itemBuilder: (context, index) { + return Container( + margin: const EdgeInsets.all(8), + constraints: BoxConstraints( + maxWidth: + MediaQuery.sizeOf(context).width * + 0.2), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0x664D4D4D), + blurRadius: 6, + offset: Offset(0, 1), + spreadRadius: 0, + ) + ], + color: Theme.of(context) + .colorScheme + .surface, + borderRadius: + BorderRadius.circular(16)), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + DefaultPlaceHolder( + child: AspectRatio( + aspectRatio: 1 / 1, + child: Container( + color: Colors.white, + height: double.infinity, + ), + ), + ), + Padding( + padding: + const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + DefaultPlaceHolder( + child: Container( + height: 12, + width: 120, + decoration: BoxDecoration( + borderRadius: + BorderRadius + .circular(16), + color: Colors.white), + ), + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + DefaultPlaceHolder( + child: Assets + .icon.outline.coin + .svg( + width: 16, + height: 16, + color: Theme.of( + context) + .colorScheme + .secondary), + ), + const SizedBox( + width: 4, + ), + DefaultPlaceHolder( + child: Container( + width: 24, + height: 12, + decoration: BoxDecoration( + borderRadius: + BorderRadius + .circular( + 16), + color: + Colors.white), + ), + ) + ], + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + DefaultPlaceHolder( + child: Container( + width: 8, + height: 8, + decoration: + BoxDecoration( + shape: + BoxShape.circle, + color: Theme.of( + context) + .colorScheme + .secondary, + ), + ), + ), + const SizedBox( + width: 8, + ), + DefaultPlaceHolder( + child: Container( + width: 46, + height: 12, + decoration: BoxDecoration( + borderRadius: + BorderRadius + .circular( + 16), + color: + Colors.white), + )) + ], + ) + ], + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ); + }, + ), + // BlocBuilder( + // builder: (context, state) { + // if (state is CoursesFail) { + // return const SizedBox.shrink(); + // } + // if (state is CoursesSuccess) { + // if (state.courses.isEmpty) { + // return const SizedBox.shrink(); + // } + // return coursesList(state); + // } + + // return coursesListPlaceholder(); + // }, + // ), + BlocBuilder( + builder: (context, state) { + if (state is PostsFail) { + return const SizedBox.shrink(); + } + if (state is PostsSuccess) { + if (kDebugMode) { + print('✅ Posts: ${state.posts.length}'); + } + if (state.posts.isEmpty) { + return const SizedBox.shrink(); + } + return postsList(state); + } + return postsListPlaceholder(); + }, + ), + const SizedBox( + height: 24, + ) + + // publicBotsList(), + // BlocBuilder( + // builder: (context, state) { + // if (state is BannersSuccess) { + // if (state.banners.length < 3) { + // return const SizedBox.shrink(); + // } + // Banners banner = state.banners[state.banners.length - 2]; + + // return bannerCard(context, banner); + // } + // if (state is BannersFail) { + // return const SizedBox.shrink(); + // } + // return DefaultPlaceHolder( + // child: Container( + // height: Responsive(context).isMobile() ? 80 : 160, + // width: double.infinity, + // padding: const EdgeInsets.all(8), + // margin: const EdgeInsets.symmetric(horizontal: 16.0) + // .copyWith(bottom: 16), + // decoration: BoxDecoration( + // color: Colors.white, + // borderRadius: BorderRadius.circular(16), + // ), + // ), + // ); + // }, + // ), + // toolsList(), + // BlocBuilder( + // builder: (context, state) { + // if (state is BannersSuccess) { + // if (state.banners.length < 3) { + // return const SizedBox.shrink(); + // } + // Banners banner = state.banners.last; + // return bannerCard(context, banner); + // } + // if (state is BannersFail) { + // return const SizedBox.shrink(); + // } + // return DefaultPlaceHolder( + // child: Container( + // height: Responsive(context).isMobile() ? 80 : 160, + // width: double.infinity, + // padding: const EdgeInsets.all(8), + // margin: const EdgeInsets.symmetric(horizontal: 16.0) + // .copyWith(bottom: 16), + // decoration: BoxDecoration( + // color: Colors.white, + // borderRadius: BorderRadius.circular(16), + // ), + // ), + // ); + // }, + // ), + // privateBotsList(), + ], + ), + ), + mobile: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + const SizedBox( + height: 16, + ), + toolsList(), + const SizedBox( + height: 16, + ), + BlocBuilder( + builder: (context, state) { + if (state is BannersSuccess) { + if (state.banners.length < 3) { + return const SizedBox.shrink(); + } + Banners banner = + state.banners[state.banners.length - 2]; + + return bannerCard(context, banner); + } + if (state is BannersFail) { + return const SizedBox.shrink(); + } + return DefaultPlaceHolder( + child: Container( + height: Responsive(context).isMobile() ? 80 : 160, + width: double.infinity, + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.symmetric(horizontal: 16.0) + .copyWith(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + ), + ); + }, + ), + + BlocBuilder( + builder: (context, state) { + if (state is BestAssistantsFail) { + return const SizedBox.shrink(); + } + if (state is BestAssistantsSuccess) { + final List allBots = state.assistants; + return Column( + children: [ + ListTile( + leading: Assets.icon.outline.assistant.svg( + color: + Theme.of(context).colorScheme.onSurface, + width: 24, + height: 24), + title: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + 'دستیارهای برگزیده', + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + ), + trailing: InkWell( + onTap: () { + screenIndex.value = 1; + initiaGlobalCatItem.value = Categories( + id: -1, name: 'برگزیده‌ها'); + context + .read() + .add(const GetGlobalAssistants()); + }, + child: Text( + 'مشاهده همه', + style: AppTextStyles.body6.copyWith( + color: Theme.of(context) + .colorScheme + .primary, + fontWeight: FontWeight.bold), + )), + ), + SizedBox( + width: double.infinity, + height: 170, + child: ListView.builder( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + shrinkWrap: true, + itemCount: min(10, allBots.length), + padding: + const EdgeInsets.symmetric(horizontal: 8), + itemBuilder: (context, index) { + return SizedBox( + width: 160, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4.0, vertical: 8), + child: InkWell( + onTap: () { + context.go(Routes.assistant, + extra: allBots[index].id); + }, + child: BotGridCard( + showCat: true, + bot: allBots[index], + )), + ), + ); + }, + ), + ), + const SizedBox( + height: 16, + ), + ], + ); + } + return Column( + children: [ + DefaultPlaceHolder( + child: ListTile( + leading: Assets.icon.outline.assistant.svg( + color: + Theme.of(context).colorScheme.onSurface, + width: 24, + height: 24), + title: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + 'دستیارهای برگزیده', + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + ), + trailing: Text( + 'مشاهده همه', + style: AppTextStyles.body6.copyWith( + color: + Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold), + ), + ), + ), + SizedBox( + width: double.infinity, + height: 170, + child: ListView.builder( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: 10, + padding: + const EdgeInsets.symmetric(horizontal: 8), + itemBuilder: (context, index) { + return const SizedBox( + width: 160, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 4.0, vertical: 8), + child: BotGridCardPlaceholder(), + ), + ); + }, + ), + ), + const SizedBox( + height: 16, + ), + ], + ); + }, + ), + BlocBuilder( + builder: (context, state) { + if (state is BannersSuccess) { + if (state.banners.length < 3) { + return const SizedBox.shrink(); + } + Banners banner = state.banners.last; + return bannerCard(context, banner); + } + if (state is BannersFail) { + return const SizedBox.shrink(); + } + return DefaultPlaceHolder( + child: Container( + height: Responsive(context).isMobile() ? 80 : 160, + width: double.infinity, + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.symmetric(horizontal: 16.0) + .copyWith(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + ), + ); + }, + ), + BlocBuilder( + builder: (context, state) { + if (state is PersonsFail) { + return const SizedBox(); + } + if (state is PersonsSuccess) { + if (state.chars.categories?.isEmpty ?? true) { + return const SizedBox.shrink(); + } + final List bots = state.chars.categories! + .expand( + (genModel) => genModel.bots!.take(2)) + .toList() + ..shuffle(); + + return Column( + children: [ + ListTile( + leading: Assets.icon.outline.mageScanUser.svg( + color: + Theme.of(context).colorScheme.onSurface, + width: 24, + height: 24), + title: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + 'گفتگو با شخصیت‌ها', + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + ), + trailing: InkWell( + onTap: () { + screenIndex.value = 3; + }, + child: Text( + 'مشاهده همه', + style: AppTextStyles.body6.copyWith( + color: Theme.of(context) + .colorScheme + .primary, + fontWeight: FontWeight.bold), + )), + ), + SizedBox( + width: double.infinity, + height: 160, + child: ListView.builder( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + itemCount: bots.length, + physics: const BouncingScrollPhysics(), + padding: + const EdgeInsets.symmetric(horizontal: 8), + itemBuilder: (context, index) { + final person = bots[index]; + return InkWell( + onTap: () { + context.go(Routes.chat, + extra: ChatArgs( + bot: person, isPerson: true)); + }, + child: Container( + margin: const EdgeInsets.all(8), + constraints: BoxConstraints( + maxWidth: MediaQuery.sizeOf(context) + .width * + 0.8), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0x664D4D4D), + blurRadius: 6, + offset: Offset(0, 1), + spreadRadius: 0, + ) + ], + color: Theme.of(context) + .colorScheme + .surface, + borderRadius: + BorderRadius.circular(16)), + child: ClipRRect( + borderRadius: + BorderRadius.circular(16), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AspectRatio( + aspectRatio: 1 / 1, + child: ImageNetwork( + url: person.image ?? '', + fit: BoxFit.cover, + radius: 0, + height: double.infinity, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all( + 16.0), + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + mainAxisAlignment: + MainAxisAlignment + .center, + children: [ + Text( + person.name ?? '', + style: AppTextStyles + .body3 + .copyWith( + color: Theme.of( + context) + .colorScheme + .onSurface, + fontWeight: + FontWeight + .bold), + maxLines: 1, + overflow: TextOverflow + .ellipsis, + ), + Row( + children: [ + Assets.icon.outline.coin.svg( + width: 16, + height: 16, + color: Theme.of( + context) + .colorScheme + .secondary), + const SizedBox( + width: 4, + ), + Text( + person.cost == 0 || + person.cost == + null + ? 'رایگان' + : person.cost + .toString(), + style: AppTextStyles.body5.copyWith( + color: Theme.of( + context) + .colorScheme + .secondary), + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + ) + ], + ), + Row( + children: [ + Container( + width: 8, + height: 8, + decoration: + BoxDecoration( + shape: BoxShape + .circle, + color: Theme.of( + context) + .colorScheme + .secondary, + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + person.category + ?.name ?? + '', + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + style: + AppTextStyles + .body5 + .copyWith( + color: Theme.of( + context) + .colorScheme + .onSurface, + )), + ) + ], + ) + ], + ), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ], + ); + } + + return Column( + children: [ + ListTile( + leading: DefaultPlaceHolder( + child: Assets.icon.outline.mageScanUser.svg( + color: + Theme.of(context).colorScheme.onSurface, + width: 24, + height: 24), + ), + title: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: DefaultPlaceHolder( + child: Text( + 'گفتگو با شخصیت‌ها', + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + ), + ), + ), + SizedBox( + width: double.infinity, + height: 160, + child: ListView.builder( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + itemCount: 10, + physics: const NeverScrollableScrollPhysics(), + padding: + const EdgeInsets.symmetric(horizontal: 8), + itemBuilder: (context, index) { + return Container( + margin: const EdgeInsets.all(8), + constraints: BoxConstraints( + maxWidth: + MediaQuery.sizeOf(context).width * + 0.8), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0x664D4D4D), + blurRadius: 6, + offset: Offset(0, 1), + spreadRadius: 0, + ) + ], + color: Theme.of(context) + .colorScheme + .surface, + borderRadius: + BorderRadius.circular(16)), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + DefaultPlaceHolder( + child: AspectRatio( + aspectRatio: 1 / 1, + child: Container( + color: Colors.white, + height: double.infinity, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + DefaultPlaceHolder( + child: Container( + height: 12, + width: 120, + decoration: BoxDecoration( + borderRadius: + BorderRadius + .circular(16), + color: Colors.white), + ), + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + DefaultPlaceHolder( + child: Assets + .icon.outline.coin + .svg( + width: 16, + height: 16, + color: Theme.of( + context) + .colorScheme + .secondary), + ), + const SizedBox( + width: 4, + ), + DefaultPlaceHolder( + child: Container( + width: 24, + height: 12, + decoration: BoxDecoration( + borderRadius: + BorderRadius + .circular( + 16), + color: + Colors.white), + ), + ) + ], + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + DefaultPlaceHolder( + child: Container( + width: 8, + height: 8, + decoration: + BoxDecoration( + shape: + BoxShape.circle, + color: + Theme.of(context) + .colorScheme + .secondary, + ), + ), + ), + const SizedBox( + width: 8, + ), + DefaultPlaceHolder( + child: Container( + width: 46, + height: 12, + decoration: BoxDecoration( + borderRadius: + BorderRadius + .circular(16), + color: Colors.white), + )) + ], + ) + ], + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ); + }, + ), + // BlocBuilder( + // builder: (context, state) { + // if (state is CoursesFail) { + // return const SizedBox.shrink(); + // } + // if (state is CoursesSuccess) { + // if (state.courses.isEmpty) { + // return const SizedBox.shrink(); + // } + // return coursesList(state); + // } + + // return coursesListPlaceholder(); + // }, + // ), + BlocBuilder( + builder: (context, state) { + if (state is PostsFail) { + return const SizedBox.shrink(); + } + if (state is PostsSuccess) { + if (state.posts.isEmpty) { + return const SizedBox.shrink(); + } + return postsList(state); + } + + return postsListPlaceholder(); + }, + ), + const SizedBox( + height: 24, + ) + + // publicBotsList(), + // BlocBuilder( + // builder: (context, state) { + // if (state is BannersSuccess) { + // if (state.banners.length < 3) { + // return const SizedBox.shrink(); + // } + // Banners banner = state.banners[state.banners.length - 2]; + + // return bannerCard(context, banner); + // } + // if (state is BannersFail) { + // return const SizedBox.shrink(); + // } + // return DefaultPlaceHolder( + // child: Container( + // height: Responsive(context).isMobile() ? 80 : 160, + // width: double.infinity, + // padding: const EdgeInsets.all(8), + // margin: const EdgeInsets.symmetric(horizontal: 16.0) + // .copyWith(bottom: 16), + // decoration: BoxDecoration( + // color: Colors.white, + // borderRadius: BorderRadius.circular(16), + // ), + // ), + // ); + // }, + // ), + // toolsList(), + // BlocBuilder( + // builder: (context, state) { + // if (state is BannersSuccess) { + // if (state.banners.length < 3) { + // return const SizedBox.shrink(); + // } + // Banners banner = state.banners.last; + // return bannerCard(context, banner); + // } + // if (state is BannersFail) { + // return const SizedBox.shrink(); + // } + // return DefaultPlaceHolder( + // child: Container( + // height: Responsive(context).isMobile() ? 80 : 160, + // width: double.infinity, + // padding: const EdgeInsets.all(8), + // margin: const EdgeInsets.symmetric(horizontal: 16.0) + // .copyWith(bottom: 16), + // decoration: BoxDecoration( + // color: Colors.white, + // borderRadius: BorderRadius.circular(16), + // ), + // ), + // ); + // }, + // ), + // privateBotsList(), + ], + ), + ), + ], + ), + ), + ), + ); + } + + SizedBox startChatSection(BuildContext context) { + return SizedBox( + child: Responsive(context).isMobile() + ? Column( + children: [ + BlocBuilder( + builder: (context, state) { + if (state is MainChatBotFail) { + return const SizedBox.shrink(); + } + return InkWell( + onTap: state is MainChatBotSuccess + ? () => context.go(Routes.chat, + extra: ChatArgs(bot: state.bot)) + : null, + child: DefaultPlaceHolder( + enabled: state is! MainChatBotSuccess, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(360), + color: Theme.of(context).colorScheme.surface), + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + child: Row( + children: [ + Expanded( + child: FittedBox( + child: Text( + 'سوالت رو همینجا بپرس تا باهم صحبت کنیم!', + textDirection: TextDirection.rtl, + style: AppTextStyles.body5.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + ), + ), + const SizedBox( + width: 24, + ), + Transform.rotate( + angle: pi, + child: CircleIconBtn( + iconColor: Colors.white, + color: Theme.of(context) + .colorScheme + .secondary, + icon: Assets.icon.bold.send)), + ], + ), + ), + ), + ); + }, + ), + ListTile( + title: Text( + 'مدیا', + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface), + ), + leading: Assets.icon.outline.media + .svg(color: Theme.of(context).colorScheme.onSurface), + // trailing: GestureDetector( + // onTap: () { + // screenIndex.value = 3; + // }, + // child: Text( + // 'مشاهده همه', + // style: AppTextStyles.body6.copyWith( + // color: Theme.of(context).colorScheme.primary, + // fontWeight: FontWeight.bold), + // ), + // ), + ), + const SizedBox( + height: 8, + ), + BlocBuilder( + builder: (context, state) { + if (state is MediasFail) { + return const SizedBox.shrink(); + } + if (state is MediasSuccess) { + final medias = state.medias.categories!; + if (medias.isEmpty) return const SizedBox.shrink(); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate( + medias.length, + (index) { + final media = medias[index]; + return Row(children: [ + mediaCard(context, + title: + media.name!.replaceAll('ساخت ', ''), + icon: media.icon!, onClick: () { + String? route; + if (media.icon!.contains('image')) { + route = Routes.generatPhoto; + } else if (media.icon!.contains('audio')) { + route = Routes.generatAudio; + } else if (media.icon!.contains('video')) { + route = Routes.generatVideo; + } + if (route != null) { + context.go(route, extra: media.id); + } + }), + if (index != medias.length - 1) + const SizedBox( + width: 16, + ) + ]); + }, + )), + ); + } + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate( + 3, + (index) => Row( + children: [ + DefaultPlaceHolder( + child: Container( + width: 95, + height: 95, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(36), + ), + )), + if (index != 2) + const SizedBox( + width: 16, + ) + ], + ), + ), + ); + }, + ), + ], + ) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + BlocBuilder( + builder: (context, state) { + if (state is MainChatBotFail) { + return const SizedBox.shrink(); + } + return InkWell( + onTap: state is MainChatBotSuccess + ? () => context.go(Routes.chat, + extra: ChatArgs(bot: state.bot)) + : null, + child: DefaultPlaceHolder( + enabled: state is! MainChatBotSuccess, + child: Container( + width: MediaQuery.sizeOf(context).width * 0.4, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(360), + color: Theme.of(context).colorScheme.surface), + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + child: Row( + children: [ + Expanded( + child: Text( + 'سوالت رو همینجا بپرس تا باهم صحبت کنیم!', + textDirection: TextDirection.rtl, + style: AppTextStyles.body2.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + ), + const SizedBox( + width: 24, + ), + Transform.rotate( + angle: pi, + child: CircleIconBtn( + iconColor: Colors.white, + size: 64, + color: Theme.of(context) + .colorScheme + .secondary, + icon: Assets.icon.bold.send)), + ], + ), + ), + ), + ); + }, + ), + const SizedBox( + height: 8, + ), + BlocBuilder( + builder: (context, state) { + if (state is MediasFail) { + return const SizedBox.shrink(); + } + if (state is MediasSuccess) { + final medias = state.medias.categories!; + if (medias.isEmpty) return const SizedBox.shrink(); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + medias.length, + (index) { + final media = medias[index]; + return Row(children: [ + mediaCard(context, + title: + media.name!.replaceAll('ساخت ', ''), + icon: media.icon!, onClick: () { + String? route; + if (media.icon!.contains('image')) { + route = Routes.generatPhoto; + } else if (media.icon!.contains('audio')) { + route = Routes.generatAudio; + } else if (media.icon!.contains('video')) { + route = Routes.generatVideo; + } + if (route != null) { + context.go(route, extra: media.id); + } + }), + if (index != medias.length - 1) + const SizedBox( + width: 16, + ) + ]); + }, + )), + ); + } + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + 3, + (index) => Row( + children: [ + DefaultPlaceHolder( + child: Container( + width: 95, + height: 95, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(36), + ), + )), + if (index != 2) + const SizedBox( + width: 16, + ) + ], + ), + ), + ); + }, + ), + ], + ), + ); + } + + Column mediaCard(BuildContext context, + {required final String title, + required final String icon, + final Function()? onClick}) { + return Column( + children: [ + InkWell( + onTap: onClick, + child: Stack( + children: [ + Container( + width: 95, + height: 95, + padding: const EdgeInsets.only(left: 4, bottom: 4), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0x664D4D4D), + blurRadius: 6, + offset: Offset(0, 1), + spreadRadius: 0, + ) + ], + borderRadius: BorderRadius.circular(36), + color: context.read().isDark() + ? Theme.of(context).colorScheme.surface + : AppColors.primaryColor[50], + border: Border( + top: BorderSide( + width: 12, + color: context.read().isDark() + ? AppColors.black[900] + : AppColors.primaryColor[100], + ), + right: BorderSide( + width: 12, + color: context.read().isDark() + ? AppColors.black[900] + : AppColors.primaryColor[100], + ))), + child: Center( + child: SizedBox( + width: 48, + height: 48, + child: ImageNetwork( + url: icon, + fit: BoxFit.cover, + )), + ), + ), + Positioned( + top: 10, + right: 16, + child: Container( + width: 4, + height: 4, + decoration: const BoxDecoration( + color: Colors.white, shape: BoxShape.circle), + )), + Positioned( + top: 14, + right: 22, + child: Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + color: Colors.white, shape: BoxShape.circle), + )), + Positioned( + top: 24, + right: 12, + child: Container( + width: 10, + height: 10, + decoration: const BoxDecoration( + color: Colors.white, shape: BoxShape.circle), + )), + ], + ), + ), + const SizedBox( + height: 8, + ), + Text( + title, + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface), + ) + ], + ); + } + + Widget bannerCard(BuildContext context, Banners banner) { + return Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16.0).copyWith(bottom: 16), + child: GestureDetector( + onTap: () async { + if (banner.bot != null) { + if (banner.bot?.tool ?? false) { + context.go(Routes.assistant, extra: banner.bot!.id); + } else { + context.go(Routes.chat, extra: ChatArgs(bot: banner.bot!)); + } + } else if (banner.link != null) { + if (banner.link!.isURL()) { + await launchUrl(Uri.parse(banner.link!), + mode: LaunchMode.externalApplication) + .onError( + (error, stackTrace) { + if (kDebugMode) { + print('error open Link is: $error'); + } + return false; + }, + ); + } else { + try { + if (banner.link!.contains('navigate')) { + int? index; + if (banner.link!.contains('home')) { + index = 0; + } else if (banner.link!.contains('assisstant')) { + index = 1; + } else if (banner.link!.contains('media')) { + index = 2; + } else if (banner.link!.contains('characters')) { + index = 3; + } else if (banner.link!.contains('setting')) { + index = 4; + } + + screenIndex.value = index!; + } else { + context.go(banner.link!); + } + } catch (e) { + if (kDebugMode) { + print('Error is: $e'); + } + } + } + } + }, + child: Builder( + builder: (context) { + if (banner.image != null) { + return Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: ImageNetwork( + width: + Responsive(context).isMobile() ? double.infinity : 600, + height: Responsive(context).isMobile() ? null : 180, + url: banner.image, + fit: BoxFit.contain, + placeholder: DefaultPlaceHolder( + child: Container( + height: Responsive(context).isMobile() ? 80 : 160, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + ), + ), + ), + ), + ); + } + return Container( + height: Responsive(context).isMobile() ? 80 : 160, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + image: DecorationImage( + opacity: 0.5, + colorFilter: ColorFilter.mode( + AppColors.green.defaultShade, BlendMode.multiply), + image: const NetworkImage( + 'https://s3-alpha-sig.figma.com/img/6353/8887/9fc27292d7cdaf98ca1632ffe66cab33?Expires=1739145600&Key-Pair-Id=APKAQ4GOSFWCW27IBOMQ&Signature=OOoLrh4qH2H~6-6Z2pMM1LnElA1c04FSNMJTJifksA~RSCjQw2H7D-QMwWUcn38BmdUFWDxN1pk6U~WL4lwy36dpHlTq9KyAeFTl-XHhO5wQT8z~xhoRqKpaizAkoiU1bb-G2BD0rQUU~CfbkdpMy8k8iBa6HfXxXiBDxhWYLluUTtCtJHpt1-cQhE2ryYY1sYCgzGZp2uM8hbAFznCwgQxNVt4gNofnLHGRCw8dZ7e2QUzv2CohTRCaV-O1IM2SU2xd44UmciD5z~3DqnxD4bLe9IPtJ22HqiDRL8pMIeNpcrqiUPPQslO9GtMXdXAe4wb8WDU3byegROvbrZ4KbA__'), + fit: BoxFit.cover), + borderRadius: BorderRadius.circular(16), + ), + ); + }, + ), + ), + ); + } + + Widget carousleSliders() { + return BlocBuilder( + builder: (context, state) { + if (state is BannersSuccess) { + final banners = [...state.banners]; + if (banners.length >= 3) { + banners.removeLast(); + banners.removeLast(); + } + + return CarousleSliderBanners( + banners: banners, + autoPlay: true, + enableInfiniteScroll: banners.length > 1, + ); + } + if (state is BannersFail) { + return const SizedBox.shrink(); + } + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (Responsive(context).isDesktop()) + Expanded( + child: DefaultPlaceHolder( + child: Container( + height: MediaQuery.sizeOf(context).height * 0.15, + margin: const EdgeInsets.all(16).copyWith(right: 0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + bottomLeft: Radius.circular(16))), + ), + ), + ), + if (Responsive(context).isDesktop() || + Responsive(context).isTablet()) + Expanded( + child: DefaultPlaceHolder( + child: Container( + height: MediaQuery.sizeOf(context).height * 0.15, + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16)), + ), + ), + ), + Expanded( + child: DefaultPlaceHolder( + child: AspectRatio( + aspectRatio: 16 / 9, + child: Container( + width: double.infinity, + height: MediaQuery.sizeOf(context).height * + (Responsive(context).isDesktop() ? 0.15 : 0.3), + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16)), + ), + ), + ), + ), + if (Responsive(context).isDesktop() || + Responsive(context).isTablet()) + Expanded( + child: DefaultPlaceHolder( + child: Container( + height: MediaQuery.sizeOf(context).height * 0.15, + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16)), + ), + ), + ), + if (Responsive(context).isDesktop()) + Expanded( + child: DefaultPlaceHolder( + child: Container( + height: MediaQuery.sizeOf(context).height * 0.15, + margin: const EdgeInsets.all(16).copyWith(left: 0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topRight: Radius.circular(16), + bottomRight: Radius.circular(16))), + ), + ), + ), + ], + ); + }, + ); + } + + Widget publicBotsList() { + return Directionality( + textDirection: TextDirection.rtl, + child: BlocBuilder( + builder: (context, state) { + if (state is BotsFail) { + return const SizedBox.shrink(); + } + if (state is BotsSuccess) { + final publicBots = state.publicBots; + if (publicBots.isEmpty) return const SizedBox.shrink(); + return Column( + children: [ + ListTile( + title: Text( + 'مدل‌های هوش مصنوعی رایگان', + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface), + ), + leading: Assets.icon.outline.gift + .svg(color: Theme.of(context).colorScheme.onSurface), + ), + SizedBox( + width: MediaQuery.sizeOf(context).width, + height: 154, + child: ListView.builder( + itemCount: publicBots.length, + physics: const BouncingScrollPhysics(), + shrinkWrap: true, + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 8), + itemBuilder: (context, index) { + final bot = publicBots[index]; + + return GestureDetector( + onTap: () { + context.go(Routes.chat, extra: ChatArgs(bot: bot)); + }, + child: publicBotCard(context, bot)); + }, + ), + ), + const SizedBox( + height: 16, + ) + ], + ); + } + + return Column( + children: [ + DefaultPlaceHolder( + child: ListTile( + title: Text( + 'مدل‌های هوش مصنوعی رایگان', + style: AppTextStyles.body4 + .copyWith(fontWeight: FontWeight.bold), + ), + leading: Assets.icon.outline.gift.svg(), + ), + ), + SizedBox( + width: MediaQuery.sizeOf(context).width, + height: 154, + child: ListView.builder( + itemCount: 10, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 8), + itemBuilder: (context, index) { + return publicBotCardPlaceholder(context); + }, + ), + ), + const SizedBox( + height: 16, + ) + ], + ); + }, + ), + ); + } + + Widget publicBotCard(BuildContext context, Bots bot) { + return Container( + margin: const EdgeInsets.all(8), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0x664D4D4D), + blurRadius: 6, + offset: Offset(0, 1), + spreadRadius: 0, + ) + ], + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16)), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + width: 60, + height: 60, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: context.read().isDark() + ? AppColors.black[900] + : AppColors.primaryColor[50] + // boxShadow: const [ + // BoxShadow( + // color: Color(0x664D4D4D), + // blurRadius: 30, + // offset: Offset(0, 1), + // spreadRadius: 0, + // ) + // ], + ), + child: ImageNetwork( + radius: 16, + color: Theme.of(context).colorScheme.onSurface, + url: bot.image), + ), + const SizedBox( + width: 8, + ), + Text( + bot.name ?? '', + style: AppTextStyles.headline6 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ) + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12) + .copyWith(bottom: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .secondary + .withAlpha(50), + borderRadius: BorderRadius.circular(8)), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: 8, + height: 8, + margin: const EdgeInsets.only(bottom: 4), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.secondary), + ), + const SizedBox( + width: 8, + ), + Text( + 'رایگان', + style: AppTextStyles.body5.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold), + ) + ], + ), + ), + const SizedBox( + width: 8, + ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .secondary + .withAlpha(50), + borderRadius: BorderRadius.circular(8)), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: 8, + height: 8, + margin: const EdgeInsets.only(bottom: 4), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.secondary), + ), + const SizedBox( + width: 8, + ), + Text( + 'بدون محدودیت', + style: AppTextStyles.body5.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold), + ) + ], + ), + ), + ], + )), + ], + ), + ); + } + + Widget publicBotCardPlaceholder( + BuildContext context, + ) { + return Container( + margin: const EdgeInsets.all(8), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0x664D4D4D), + blurRadius: 6, + offset: Offset(0, 1), + spreadRadius: 0, + ) + ], + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16)), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + DefaultPlaceHolder( + child: Container( + width: 54, + height: 54, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Colors.white, + // boxShadow: const [ + // BoxShadow( + // color: Color(0x664D4D4D), + // blurRadius: 30, + // offset: Offset(0, 1), + // spreadRadius: 0, + // ) + // ], + ), + ), + ), + const SizedBox( + width: 8, + ), + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8)), + child: Text( + 'bot.name ', + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + ), + ) + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12) + .copyWith(bottom: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + DefaultPlaceHolder( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8)), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: 8, + height: 8, + margin: const EdgeInsets.only(bottom: 4), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.secondary), + ), + const SizedBox( + width: 8, + ), + Text( + 'رایگان', + style: AppTextStyles.body5.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold), + ) + ], + ), + ), + ), + const SizedBox( + width: 8, + ), + DefaultPlaceHolder( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8)), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: 8, + height: 8, + margin: const EdgeInsets.only(bottom: 4), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.secondary), + ), + const SizedBox( + width: 8, + ), + Text( + 'بدون محدودیت', + style: AppTextStyles.body5.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold), + ) + ], + ), + ), + ), + ], + )), + ], + ), + ); + } + + Widget toolsList() { + return Directionality( + textDirection: TextDirection.rtl, + child: BlocBuilder( + builder: (context, state) { + if (state is ToolsFail || state is ToolsEmpty) { + return const SizedBox.shrink(); + } + if (state is ToolsSuccess) { + final privateBots = state.categories; + return Column( + children: [ + ListTile( + title: Text( + 'حرفه‌ای‌ترین ابزارها', + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface), + ), + leading: Assets.icon.outline.toolBox + .svg(color: Theme.of(context).colorScheme.onSurface), + trailing: GestureDetector( + onTap: () => context.go(Routes.tools), + child: Text( + 'مشاهده همه', + style: AppTextStyles.body6.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold), + ), + ), + ), + SizedBox( + width: double.infinity, + height: 158, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: privateBots.length, + shrinkWrap: true, + padding: const EdgeInsets.symmetric(horizontal: 8), + physics: const BouncingScrollPhysics(), + itemBuilder: (context, index) { + final cat = privateBots[index]; + return ToolCard(cat: cat); + }, + ), + ), + const SizedBox( + height: 16, + ) + ], + ); + } + + return Column( + children: [ + DefaultPlaceHolder( + child: ListTile( + title: Text( + 'حرفه‌ای‌ترین ابزارها', + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface), + ), + leading: Assets.icon.outline.crown + .svg(color: Theme.of(context).colorScheme.onSurface), + ), + ), + SizedBox( + width: MediaQuery.sizeOf(context).width, + height: 168, + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: 10, + physics: const NeverScrollableScrollPhysics(), + scrollDirection: Axis.horizontal, + shrinkWrap: true, + itemBuilder: (context, index) { + return const ToolCardPlaceholder(); + }, + ), + ), + ], + ); + }, + ), + ); + } + + Widget privateBotsList() { + return Directionality( + textDirection: TextDirection.rtl, + child: BlocBuilder( + builder: (context, state) { + if (state is BotsFail) { + return const SizedBox.shrink(); + } + if (state is BotsSuccess) { + final privateBots = state.privateBots; + if (privateBots.isEmpty) return const SizedBox.shrink(); + return Column( + children: [ + ListTile( + title: Text( + 'مدل‌های هوش مصنوعی حرفه‌ای', + style: AppTextStyles.body4 + .copyWith(fontWeight: FontWeight.bold), + ), + leading: Assets.icon.outline.crown + .svg(color: Theme.of(context).colorScheme.onSurface), + ), + + SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: List.generate( + (privateBots.length / 2).ceil(), + (index) { + final bot = privateBots[index]; + return GestureDetector( + onTap: () { + context.go(Routes.chat, + extra: ChatArgs(bot: bot)); + }, + child: privateBotCard(context, bot)); + }, + ), + ), + Row( + children: List.generate( + (privateBots.length / 2).floor(), + (i) { + final index = + (((privateBots.length) / 2).ceil() + i); + final bot = privateBots[index]; + return GestureDetector( + onTap: () { + context.go(Routes.chat, + extra: ChatArgs(bot: bot)); + }, + child: privateBotCard(context, bot)); + }, + ), + ), + ], + ), + ), + // SizedBox( + // width: MediaQuery.sizeOf(context).width, + // height: 108 * 2, + // child: GridView.count( + // padding: const EdgeInsets.symmetric(horizontal: 8), + // crossAxisCount: 2, + // physics: const BouncingScrollPhysics(), + // scrollDirection: Axis.horizontal, + // shrinkWrap: true, + // childAspectRatio: 0.4, + // children: List.generate( + // privateBots.length, + // (index) { + // final bot = privateBots[index]; + // return GestureDetector( + // onTap: () { + // context.go(Routes.chat, + // extra: ChatArgs(bot: bot)); + // }, + // child: privateBotCard(context, bot)); + // }, + // ), + // ), + // ), + const SizedBox( + height: 16, + ) + ], + ); + } + + return Column( + children: [ + DefaultPlaceHolder( + child: ListTile( + title: Text( + 'مدل‌های هوش مصنوعی حرفه‌ای', + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface), + ), + leading: Assets.icon.outline.crown + .svg(color: Theme.of(context).colorScheme.onSurface), + ), + ), + SizedBox( + width: MediaQuery.sizeOf(context).width, + height: 108, + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: 10, + physics: const NeverScrollableScrollPhysics(), + scrollDirection: Axis.horizontal, + shrinkWrap: true, + itemBuilder: (context, index) { + return privateBotCardPlaceholder(context); + }, + ), + ), + const SizedBox( + height: 16, + ) + ], + ); + }, + ), + ); + } + + Widget privateBotCard(BuildContext context, Bots bot) { + return Container( + margin: const EdgeInsets.all(8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0x664D4D4D), + blurRadius: 6, + offset: Offset(0, 1), + spreadRadius: 0, + ) + ], + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16)), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + width: 60, + height: 60, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: context.read().isDark() + ? AppColors.black[900] + : AppColors.primaryColor[50] + // boxShadow: const [ + // BoxShadow( + // color: Color(0x664D4D4D), + // blurRadius: 30, + // offset: Offset(0, 1), + // spreadRadius: 0, + // ) + // ], + ), + child: ImageNetwork( + radius: 16, + color: Theme.of(context).colorScheme.onSurface, + url: bot.image), + ), + const SizedBox( + width: 8, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + bot.name ?? '', + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + const SizedBox( + height: 4, + ), + Row( + children: [ + Assets.icon.outline.coin.svg( + color: Theme.of(context).colorScheme.primary, + width: 18, + height: 18), + Text( + bot.cost == 0 ? 'رایگان' : 'هر پیام ${bot.cost} سکه', + style: AppTextStyles.body5.copyWith( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900]), + ), + ], + ), + ], + ) + ], + ), + ], + ), + ); + } + + Widget privateBotCardPlaceholder(BuildContext context) { + return Container( + margin: const EdgeInsets.all(8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0x664D4D4D), + blurRadius: 6, + offset: Offset(0, 1), + spreadRadius: 0, + ) + ], + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16)), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + DefaultPlaceHolder( + child: Container( + width: 54, + height: 54, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Colors.white, + // boxShadow: const [ + // BoxShadow( + // color: Color(0x664D4D4D), + // blurRadius: 30, + // offset: Offset(0, 1), + // spreadRadius: 0, + // ) + // ], + ), + ), + ), + const SizedBox( + width: 8, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8)), + child: Text( + "bot.name ", + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + ), + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + DefaultPlaceHolder( + child: Assets.icon.outline.coin.svg( + color: Theme.of(context).colorScheme.primary, + width: 18, + height: 18), + ), + DefaultPlaceHolder( + child: Text( + 'هر پیام ...', + style: AppTextStyles.body5.copyWith( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900]), + ), + ), + ], + ), + ], + ) + ], + ), + ], + ), + ); + } + + Widget coursesList(CoursesSuccess state) { + return Directionality( + textDirection: TextDirection.rtl, + child: Column( + children: [ + ListTile( + title: Text( + 'رسانه هوشان', + style: AppTextStyles.body4.copyWith(fontWeight: FontWeight.bold), + ), + leading: Assets.icon.outline.hat + .svg(color: Theme.of(context).colorScheme.onSurface), + trailing: GestureDetector( + onTap: () async { + await launchUrl( + Uri.parse( + 'https://houshan.ai/%d8%a8%d9%84%d8%a7%da%af/'), + mode: LaunchMode.externalApplication) + .onError( + (error, stackTrace) { + if (kDebugMode) { + print('error open Link is: $error'); + } + return false; + }, + ); + }, + child: Text( + 'مشاهده همه', + style: AppTextStyles.body6.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold), + ), + ), + ), + SizedBox( + width: double.infinity, + height: 280, + child: ListView.builder( + itemCount: state.courses.length, + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 8), + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + final course = state.courses[index]; + return GestureDetector( + onTap: () async { + await launchUrl(Uri.parse(course.permalink ?? ''), + mode: LaunchMode.externalApplication) + .onError( + (error, stackTrace) { + if (kDebugMode) { + print('error open Link is: $error'); + } + return false; + }, + ); + }, + child: courseCard(context, course)); + }, + ), + ), + ], + ), + ); + } + + Widget postsList(PostsSuccess state) { + return Directionality( + textDirection: TextDirection.rtl, + child: Column( + children: [ + ListTile( + title: Text( + 'رسانه هوشان', + style: AppTextStyles.body4.copyWith(fontWeight: FontWeight.bold), + ), + leading: Assets.icon.outline.documentText + .svg(color: Theme.of(context).colorScheme.onSurface), + trailing: GestureDetector( + onTap: () async { + await launchUrl( + Uri.parse( + 'https://houshan.ai/%d8%a8%d9%84%d8%a7%da%af/'), + mode: LaunchMode.externalApplication) + .onError( + (error, stackTrace) { + if (kDebugMode) { + print('error open Link is: $error'); + } + return false; + }, + ); + }, + child: Text( + 'مشاهده همه', + style: AppTextStyles.body6.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold), + ), + ), + ), + SizedBox( + width: double.infinity, + height: 280, + child: ListView.builder( + itemCount: state.posts.length, + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 8), + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + final post = state.posts[index]; + return GestureDetector( + onTap: () async { + await launchUrl(Uri.parse(post.link ?? ''), + mode: LaunchMode.externalApplication) + .onError( + (error, stackTrace) { + if (kDebugMode) { + print('error open Link is: $error'); + } + return false; + }, + ); + }, + child: postsCard(context, post)); + }, + ), + ), + ], + ), + ); + } + + Widget postsListPlaceholder() { + return Directionality( + textDirection: TextDirection.rtl, + child: Column( + children: [ + ListTile( + title: DefaultPlaceHolder( + child: Text( + 'رسانه هوشان', + style: + AppTextStyles.body4.copyWith(fontWeight: FontWeight.bold), + ), + ), + leading: DefaultPlaceHolder( + child: Assets.icon.outline.documentText + .svg(color: Theme.of(context).colorScheme.onSurface), + ), + ), + SizedBox( + width: double.infinity, + height: 280, + child: ListView.builder( + itemCount: 10, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 8), + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + return courseCardPlaceholder(context); + }, + ), + ), + ], + ), + ); + } + + Widget courseCardPlaceholder(BuildContext context) { + return Container( + width: 250, + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.all(8), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0x664D4D4D), + blurRadius: 6, + offset: Offset(0, 1), + spreadRadius: 0, + ) + ], + borderRadius: BorderRadius.circular(16), + color: Theme.of(context).colorScheme.surface, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultPlaceHolder( + child: AspectRatio( + aspectRatio: 16 / 9, + child: Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8)), + )), + ), + const SizedBox(height: 8), + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8)), + child: Text( + "course.name", + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + const SizedBox(height: 4), + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8)), + child: Text( + "course.name dsadsasd sad ds asd as dasd sd ", + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + IgnorePointer( + ignoring: true, + child: LoadingButton( + onPressed: () {}, + width: double.infinity, + loading: true, + color: AppColors.gray[800], + child: Text( + 'مشاهده دوره', + style: AppTextStyles.body4.copyWith(color: Colors.white), + )), + ) + ], + ), + ); + } + + Widget coursesListPlaceholder() { + return Directionality( + textDirection: TextDirection.rtl, + child: Column( + children: [ + ListTile( + title: DefaultPlaceHolder( + child: Text( + 'دوره‌های هوش مصنوعی', + style: + AppTextStyles.body4.copyWith(fontWeight: FontWeight.bold), + ), + ), + leading: DefaultPlaceHolder( + child: Assets.icon.outline.hat + .svg(color: Theme.of(context).colorScheme.onSurface), + ), + ), + SizedBox( + width: double.infinity, + height: 280, + child: ListView.builder( + itemCount: 10, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 8), + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + return courseCardPlaceholder(context); + }, + ), + ), + ], + ), + ); + } + + Widget courseCard(BuildContext context, Courses course) { + return Container( + width: 250, + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.all(8), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0x664D4D4D), + blurRadius: 6, + offset: Offset(0, 1), + spreadRadius: 0, + ) + ], + borderRadius: BorderRadius.circular(16), + color: Theme.of(context).colorScheme.surface, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: AspectRatio( + aspectRatio: 16 / 9, + child: ImageNetwork( + radius: 12, + url: course.images != null && course.images!.isNotEmpty + ? course.images?.single.src + : null)), + ), + const SizedBox(height: 8), + Text( + course.name ?? '', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + IgnorePointer( + ignoring: true, + child: LoadingButton( + onPressed: () {}, + width: double.infinity, + child: Text( + 'مشاهده دوره', + style: AppTextStyles.body4.copyWith(color: Colors.white), + )), + ) + ], + ), + ); + } + + Widget postsCard(BuildContext context, PostsModel post) { + return Container( + width: 250, + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.all(8), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0x664D4D4D), + blurRadius: 6, + offset: Offset(0, 1), + spreadRadius: 0, + ) + ], + borderRadius: BorderRadius.circular(16), + color: Theme.of(context).colorScheme.surface, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: AspectRatio( + aspectRatio: 16 / 9, + child: ImageNetwork( + radius: 12, + url: post.featuredImageSrc ?? + post.content?.rendered?.getFirstmageTag())), + ), + const SizedBox(height: 8), + Text( + post.title?.rendered ?? '', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + IgnorePointer( + ignoring: true, + child: LoadingButton( + onPressed: () {}, + width: double.infinity, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Assets.icon.outline.library.svg(color: Colors.white), + const SizedBox( + width: 4, + ), + Text( + 'ادامه مطلب', + style: AppTextStyles.body4.copyWith(color: Colors.white), + ), + ], + )), + ) + ], + ), + ); + } +} diff --git a/lib/ui/screens/main/home_page.dart b/lib/ui/screens/main/home_page.dart new file mode 100644 index 0000000..0effe89 --- /dev/null +++ b/lib/ui/screens/main/home_page.dart @@ -0,0 +1,322 @@ +// ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package + +import 'dart:math'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/core/services/ad/tapsell_service.dart'; +import 'package:hoshan/data/model/home_args.dart'; +import 'package:hoshan/data/model/home_navbar_model.dart'; +import 'package:hoshan/data/storage/shared_preferences_helper.dart'; +import 'package:hoshan/ui/screens/main/assistant/assistant_screen.dart'; +import 'package:hoshan/ui/screens/main/home/home_screen.dart'; +import 'package:hoshan/ui/screens/main/generate_media_screen.dart'; +import 'package:hoshan/ui/screens/main/persons/persons_screen.dart'; +import 'package:hoshan/ui/screens/setting/setting_page.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/dialog/dialog_handler.dart'; +import 'package:hoshan/ui/widgets/sections/header/home_appbar.dart'; +import 'package:share_plus/share_plus.dart'; + +ValueNotifier screenIndex = ValueNotifier(0); +final searchFocus = FocusNode(); + +class HomePage extends StatefulWidget { + final HomeArgs? args; + const HomePage({super.key, this.args}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + ValueNotifier visibleAttach = ValueNotifier(false); + ValueNotifier visibleRecorder = ValueNotifier(false); + + final List navIcons = [ + HomeNavbar(title: 'خانه', icon: 'home', enabled: true), + HomeNavbar(title: 'دستیارها', icon: 'assistant', enabled: false), + HomeNavbar(title: 'مدیا', icon: 'media', enabled: false), + HomeNavbar(title: 'شخصیت‌ها', icon: 'characters', enabled: false), + HomeNavbar(title: 'تنظیمات', icon: 'setting', enabled: false), + ]; + + final TextEditingController message = TextEditingController(); + + PreferredSizeWidget? getAppBAr(int indexed) { + PreferredSizeWidget? preferredSizeWidget; + if (indexed != 1) { + preferredSizeWidget = HomeAppbar( + context, + ); + } + return preferredSizeWidget; + } + + @override + void initState() { + super.initState(); + // if (!kIsWeb) { + // try { + // final ad = AdiveryService(); + // ad.showInterstitial(); + // } catch (e) { + // if (kDebugMode) { + // print('Error initializing Adivery: $e'); + // } + // } + // } + screenIndex.value = 0; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (widget.args != null) { + if (widget.args!.message != null) { + DialogHandler(context: context) + .showWellcome(mesasge: widget.args!.message!); + } + final iso = InvitePopupStorage.getLastTime(); + final now = DateTime.now(); + final nowId = int.parse( + '${now.year}${now.month.toString().padLeft(2, '0')}${now.day.toString().padLeft(2, '0')}'); + int? dateId; + try { + final date = DateTime.parse(iso); + + dateId = int.parse( + '${date.year}${date.month.toString().padLeft(2, '0')}${date.day.toString().padLeft(2, '0')}'); + } catch (e) { + if (kDebugMode) { + print('Error when show heart: $e'); + } + } + if (dateId != nowId && Random().nextBool()) { + DialogHandler(context: context).showHeart( + onConfirm: () async { + try { + await Share.share('هوشان؛ دوست هوش مصنوعی تو\n' + 'هر سوالی داری، هر تصمیمی که می‌خوای بگیری یا هر ایده‌ی خلاقانه‌ای که داری و می‌خوای به واقعیت تبدیلش کنی، هوشان همیشه هست تا کمکت کنه!\n' + 'کد معرف: ${UserInfoCubit.userInfoModel.code}\n' + 'https://houshan.ai/'); + } catch (e) { + if (kDebugMode) { + print('Error in share Text: $e'); + } + } + }, + ); + InvitePopupStorage.setLastTime(now.toIso8601String()); + } + } + TapsellService.showAdBanner(context); + }); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: screenIndex, + builder: (context, indexed, _) { + return WillPopScope( + onWillPop: () async { + if (indexed == 0) { + // if (HomeCubit.chatId.value != null) { + // HomeCubit.chatId.value = null; + // HomeCubit.bot.value = null; + // context.read().clearItems(); + // ChatbotRepository.cancelToken?.cancel(); + // return false; + // } else { + final exit = DialogHandler(context: context).showExit(); + return exit; + // } + } else { + screenIndex.value = 0; + // mainPageController.jumpToPage(screenIndex.value); + + return false; + } + }, + child: Scaffold( + resizeToAvoidBottomInset: indexed == 0, + appBar: HomeAppbar( + context, + ), + body: Responsive(context).builder( + mobile: mainIndexed(indexed), + desktop: Row( + children: [ + Expanded( + flex: 3, + child: mainIndexed(indexed), + ), + Expanded(child: drawerNavigationBar()), + ], + ), + ), + bottomNavigationBar: Responsive(context).isMobile() + ? homeBottomNavigationBar() + : null, + ), + ); + }); + } + + Widget mainIndexed(int indexed) { + return IndexedStack( + index: indexed, + children: const [ + HomeScreen(), + AssistantScreen(), + GenerateMediaScreen(), + PersonsScreen(), + SettingPage(), + ], + ); + } + + Widget homeBottomNavigationBar() { + return ValueListenableBuilder( + valueListenable: screenIndex, + builder: (context, indexed, _) { + for (var element in navIcons) { + element.setEnabled(false); + } + navIcons[indexed].setEnabled(true); + return Directionality( + textDirection: TextDirection.rtl, + child: Container( + margin: const EdgeInsets.fromLTRB(16, 0, 16, 24), + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: const Color(0xFF000000).withOpacity(0.10), + offset: const Offset(0, 20), + blurRadius: 46, + spreadRadius: 0, + ), + ]), + child: Row( + children: List.generate(navIcons.length, (index) { + final navIcon = navIcons[index]; + + return Expanded( + child: InkWell( + borderRadius: BorderRadius.circular(100), + onTap: () async { + if (index == indexed) return; + screenIndex.value = index; + // mainPageController.jumpToPage(index); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + navIcon.getIcon(context).svg( + color: index == indexed + ? null + : Theme.of(context).colorScheme.onSurface), + if (navIcon.enabled) + Text( + navIcon.title, + style: AppTextStyles.body6.copyWith( + color: navIcon.enabled + ? context + .read() + .state == + ThemeMode.dark + ? Colors.white + : AppColors.primaryColor[800] + : AppColors.gray.defaultShade), + ) + ], + )), + ); + }), + ), + ), + ); + }); + } + + Widget drawerNavigationBar() { + return ValueListenableBuilder( + valueListenable: screenIndex, + builder: (context, indexed, _) { + for (var element in navIcons) { + element.setEnabled(false); + } + navIcons[indexed].setEnabled(true); + return Directionality( + textDirection: TextDirection.rtl, + child: Container( + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: [ + BoxShadow( + color: const Color(0xFF000000).withOpacity(0.10), + offset: const Offset(0, 20), + blurRadius: 46, + spreadRadius: 0, + ), + ]), + child: Padding( + padding: EdgeInsets.only( + top: indexed == 1 + ? kToolbarHeight + + (Responsive(context).isMobile() ? 12 : 32) + : 0), + child: Column( + children: List.generate(navIcons.length, (index) { + final navIcon = navIcons[index]; + + return Column( + children: [ + InkWell( + borderRadius: BorderRadius.circular(100), + onTap: () { + if (index == indexed) return; + screenIndex.value = index; + // mainPageController.jumpToPage(index); + }, + child: ListTile( + leading: navIcon.getIcon(context).svg( + width: 32, + height: 32, + color: index == indexed + ? null + : navIcon.enabled + ? Theme.of(context) + .colorScheme + .onSurface + : AppColors.gray[700]), + title: Text( + navIcon.title, + style: AppTextStyles.headline5.copyWith( + color: navIcon.enabled + ? context.read().isDark() + ? Theme.of(context) + .colorScheme + .onSurface + : AppColors.primaryColor[800] + : AppColors.gray[700]), + ), + ), + ), + if (index != navIcons.length - 1) const Divider() + ], + ); + }), + ), + ), + ), + ); + }); + } +} diff --git a/lib/ui/screens/main/persons/cubit/persons_cubit.dart b/lib/ui/screens/main/persons/cubit/persons_cubit.dart new file mode 100644 index 0000000..f2ae51c --- /dev/null +++ b/lib/ui/screens/main/persons/cubit/persons_cubit.dart @@ -0,0 +1,25 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/photo_gen_model.dart'; +import 'package:hoshan/data/repository/bot_repository.dart'; + +part 'persons_state.dart'; + +class PersonsCubit extends Cubit { + PersonsCubit() : super(PersonsInitial()); + + void getCharacters() async { + emit(PersonsLoading()); + try { + final response = await BotRepository.getAllCharachters(); + emit(PersonsSuccess(chars: response)); + } on DioException catch (e) { + emit(PersonsFail()); + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } +} diff --git a/lib/ui/screens/main/persons/cubit/persons_state.dart b/lib/ui/screens/main/persons/cubit/persons_state.dart new file mode 100644 index 0000000..a393185 --- /dev/null +++ b/lib/ui/screens/main/persons/cubit/persons_state.dart @@ -0,0 +1,20 @@ +part of 'persons_cubit.dart'; + +sealed class PersonsState extends Equatable { + const PersonsState(); + + @override + List get props => []; +} + +final class PersonsInitial extends PersonsState {} + +final class PersonsSuccess extends PersonsState { + final GensModel chars; + + const PersonsSuccess({required this.chars}); +} + +final class PersonsFail extends PersonsState {} + +final class PersonsLoading extends PersonsState {} diff --git a/lib/ui/screens/main/persons/persons_screen.dart b/lib/ui/screens/main/persons/persons_screen.dart new file mode 100644 index 0000000..34638c2 --- /dev/null +++ b/lib/ui/screens/main/persons/persons_screen.dart @@ -0,0 +1,481 @@ +// ignore_for_file: deprecated_member_use + +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/data/model/chat_args.dart'; +import 'package:hoshan/data/model/photo_gen_model.dart'; +import 'package:hoshan/ui/screens/main/persons/cubit/persons_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; + +class PersonsScreen extends StatefulWidget { + const PersonsScreen({super.key}); + + @override + State createState() => _PersonsScreenState(); +} + +class _PersonsScreenState extends State { + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.rtl, + child: BlocBuilder( + builder: (context, state) { + if (state is PersonsSuccess) { + if (state.chars.categories?.isEmpty ?? true) { + return SizedBox.shrink(); + } + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Responsive(context).maxWidthInDesktop( + maxWidth: 1200, + child: (contxet, mw) => Column( + children: [ + Responsive(context).isMobile() + ? ListView.builder( + itemCount: state.chars.categories?.length ?? 0, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.only(bottom: 32.0), + child: personsCat(context, + state.chars.categories![index], mw), + ); + }) + : GridView.builder( + itemCount: state.chars.categories?.length ?? 0, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2), + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.only(bottom: 32.0), + child: personsCat(context, + state.chars.categories![index], mw / 2), + ); + }, + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Assets.icon.outline.messageQuestion.svg( + color: Theme.of(context).colorScheme.onSurface), + const SizedBox( + width: 8, + ), + Text.rich( + TextSpan( + children: [ + const TextSpan( + text: 'جای کدوم شخصیت اینجا خالیه؟ '), + TextSpan( + text: 'بهمون بگو.', + recognizer: TapGestureRecognizer() + ..onTap = () { + // DialogHandler(context: context) + // .showPersonsAlert(); + context.go(Routes.ticket); + }, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context) + .colorScheme + .primary)), + ], + ), + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ) + ], + ), + ), + ], + ), + ), + ); + } + if (state is PersonsFail) { + return SizedBox.shrink(); + } + return const Center( + child: CircularProgressIndicator(), + ); + }, + ), + ); + } + + Column personsCat(BuildContext context, GenModel item, double maxWidth) { + final PageController pageController = PageController(initialPage: 0); + final ValueNotifier currentPageNotifier = ValueNotifier(0); + final ValueNotifier currentSliderNotifier = ValueNotifier(0); + return Column( + children: [ + ListTile( + leading: SvgPicture.network( + DioService.baseURL + item.icon!, + color: Theme.of(context).colorScheme.onSurface, + ), + title: Text( + item.name ?? '', + style: AppTextStyles.headline6 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: ValueListenableBuilder( + valueListenable: currentPageNotifier, + builder: (context, currentPage, child) { + return InkWell( + onTap: () { + if (currentPage == 0) { + pageController.animateToPage(1, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut); + } else { + pageController.animateToPage(0, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut); + } + }, + child: Text( + currentPage != 0 ? 'بازگشت' : 'همه', + style: AppTextStyles.body5 + .copyWith(color: Theme.of(context).colorScheme.primary), + ), + ); + }, + ), + ), + const SizedBox( + height: 8, + ), + SizedBox( + width: maxWidth, + height: maxWidth * 0.8, + child: PageView( + controller: pageController, + onPageChanged: (value) { + currentPageNotifier.value = value; + }, + physics: + const NeverScrollableScrollPhysics(), // Disable swipe, use button + children: [ + // Second Widget + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: CarouselSlider.builder( + itemCount: item.bots?.length ?? 0, + itemBuilder: (context, index, realIndex) { + if (item.bots == null || item.bots!.isEmpty) { + return SizedBox(); + } + final bot = item.bots![index]; + return Padding( + padding: const EdgeInsets.all(8.0), + child: InkWell( + onTap: () { + context.go(Routes.chat, + extra: ChatArgs(bot: bot, isPerson: true)); + }, + child: Stack( + children: [ + ImageNetwork( + width: double.infinity, + height: double.infinity, + radius: 16, + url: bot.image ?? ''), + Positioned.fill( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context) + .colorScheme + .surface + .withAlpha(40), + AppColors.primaryColor[600] + .withAlpha(180) + ])), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + bot.name ?? '', + style: AppTextStyles.body6.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold), + ), + Row( + mainAxisAlignment: + MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only( + top: 4.0), + child: Text( + bot.cost == 0 || + bot.cost == null + ? 'رایگان' + : bot.cost.toString(), + style: AppTextStyles.body6 + .copyWith( + color: Colors.white), + ), + ), + const SizedBox( + width: 4, + ), + Assets.icon.outline.coin.svg( + color: Colors.white, + width: 14, + height: 14) + ], + ) + ], + ), + )) + ], + ), + ), + ); + }, + options: CarouselOptions( + scrollPhysics: const NeverScrollableScrollPhysics(), + height: double.infinity, + autoPlay: true, + aspectRatio: 1 / 1, + viewportFraction: 1 / 3, + autoPlayInterval: const Duration(seconds: 3), + autoPlayAnimationDuration: + const Duration(milliseconds: 800), + scrollDirection: Axis.vertical, + enableInfiniteScroll: true, + onPageChanged: (index, reason) { + currentSliderNotifier.value = index; + }, + ), + ), + ), + SizedBox( + width: maxWidth * 0.6, + child: ValueListenableBuilder( + valueListenable: currentSliderNotifier, + builder: (context, index, child) { + final bot = item.bots![index]; + return AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + transitionBuilder: + (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, child: child); + }, + child: Column( + key: ValueKey(index), + children: [ + Expanded( + child: Stack( + children: [ + ImageNetwork( + width: double.infinity, + height: double.infinity, + radius: 16, + url: bot.image ?? ''), + Positioned.fill( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context) + .colorScheme + .surface + .withAlpha(40), + AppColors.primaryColor[600] + .withAlpha(180) + ])), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + Padding( + padding: + const EdgeInsets.only( + top: 4.0), + child: Text( + bot.cost == 0 || + bot.cost == null + ? 'رایگان' + : bot.cost.toString(), + style: AppTextStyles.body3 + .copyWith( + color: + Colors.white), + ), + ), + const SizedBox( + width: 4, + ), + Assets.icon.outline.coin.svg( + color: Colors.white, + width: 18, + height: 18) + ], + ) + ], + ), + )) + ], + )), + const SizedBox( + height: 12, + ), + Row( + children: [ + Expanded( + child: LoadingButton( + onPressed: () { + context.go(Routes.chat, + extra: ChatArgs( + bot: bot, isPerson: true)); + }, + width: double.infinity, + child: Text( + 'گپ با ${bot.name ?? ''}', + style: AppTextStyles.body4 + .copyWith(color: Colors.white), + ), + ), + ), + ], + ) + ], + ), + ); + }), + ), + ], + ), + ), + + SizedBox( + height: maxWidth, + child: GridView.builder( + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + childAspectRatio: 1 / 1, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + maxCrossAxisExtent: maxWidth / 2), + scrollDirection: Axis.horizontal, + shrinkWrap: true, + itemCount: item.bots?.length ?? 0, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 8), + itemBuilder: (context, index) { + final bot = item.bots![index]; + return InkWell( + onTap: () { + context.go(Routes.chat, + extra: ChatArgs(bot: bot, isPerson: true)); + }, + child: Stack( + children: [ + ImageNetwork(radius: 16, url: bot.image ?? ''), + Positioned.fill( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context) + .colorScheme + .surface + .withAlpha(40), + AppColors.primaryColor[600].withAlpha(180) + ])), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + bot.name ?? '', + style: AppTextStyles.body6.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold), + ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + bot.cost == 0 || bot.cost == null + ? 'رایگان' + : bot.cost.toString(), + style: AppTextStyles.body6 + .copyWith(color: Colors.white), + ), + ), + const SizedBox( + width: 4, + ), + Assets.icon.outline.coin.svg( + color: Colors.white, + width: 14, + height: 14) + ], + ) + ], + ), + )) + ], + ), + ); + }, + ), + ), + ], + ), + ) + ], + ); + } +} diff --git a/lib/ui/screens/purchase_page/bloc/plans_bloc.dart b/lib/ui/screens/purchase_page/bloc/plans_bloc.dart new file mode 100644 index 0000000..4f0e47c --- /dev/null +++ b/lib/ui/screens/purchase_page/bloc/plans_bloc.dart @@ -0,0 +1,28 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/plans_model.dart'; +import 'package:hoshan/data/repository/paymant_repository.dart'; + +part 'plans_event.dart'; +part 'plans_state.dart'; + +class PlansBloc extends Bloc { + PlansBloc() : super(PlansInitial()) { + on((event, emit) async { + if (event is GetAllPlans) { + emit(PlansLoading()); + try { + final plans = await PaymantRepository.getPaymantPlans(); + emit(PlansSuccess(plans: plans)); + } on DioException catch (e) { + emit(PlansFail()); + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } + }); + } +} diff --git a/lib/ui/screens/purchase_page/bloc/plans_event.dart b/lib/ui/screens/purchase_page/bloc/plans_event.dart new file mode 100644 index 0000000..6ceda0b --- /dev/null +++ b/lib/ui/screens/purchase_page/bloc/plans_event.dart @@ -0,0 +1,10 @@ +part of 'plans_bloc.dart'; + +sealed class PlansEvent extends Equatable { + const PlansEvent(); + + @override + List get props => []; +} + +class GetAllPlans extends PlansEvent{} diff --git a/lib/ui/screens/purchase_page/bloc/plans_state.dart b/lib/ui/screens/purchase_page/bloc/plans_state.dart new file mode 100644 index 0000000..ccdd496 --- /dev/null +++ b/lib/ui/screens/purchase_page/bloc/plans_state.dart @@ -0,0 +1,20 @@ +part of 'plans_bloc.dart'; + +sealed class PlansState extends Equatable { + const PlansState(); + + @override + List get props => []; +} + +final class PlansInitial extends PlansState {} + +final class PlansLoading extends PlansState {} + +final class PlansFail extends PlansState {} + +final class PlansSuccess extends PlansState { + final List plans; + + const PlansSuccess({required this.plans}); +} diff --git a/lib/ui/screens/purchase_page/purchase_page.dart b/lib/ui/screens/purchase_page/purchase_page.dart new file mode 100644 index 0000000..3a3fe8d --- /dev/null +++ b/lib/ui/screens/purchase_page/purchase_page.dart @@ -0,0 +1,230 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/data/model/ai/credit_model.dart'; +import 'package:hoshan/data/model/plans_model.dart'; +import 'package:hoshan/data/model/purchase_args.dart'; +import 'package:hoshan/ui/screens/purchase_page/bloc/plans_bloc.dart'; +import 'package:hoshan/ui/screens/setting/bloc/paymant_history_bloc.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/widgets/components/dialog/dialog_handler.dart'; +import 'package:hoshan/ui/widgets/components/purchase/purchase_card.dart'; +import 'package:hoshan/ui/widgets/components/purchase/purchase_card_placeholder.dart'; +import 'package:hoshan/ui/widgets/sections/header/reversible_appbar.dart'; + +class PurchasePage extends StatefulWidget { + final PurchaseArgs? args; + const PurchasePage({super.key, this.args}); + + @override + State createState() => _PurchasePageState(); +} + +class _PurchasePageState extends State { + @override + void initState() { + try { + if (widget.args != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + DialogHandler(context: context).showPurchStatus( + success: widget.args!.success, detail: widget.args!.message); + context.read().add(GetAllHistory()); + if (widget.args!.success) { + context + .read() + .changeCredit(CreditModel(credit: widget.args!.credit)); + } + }); + } + } catch (e) { + if (kDebugMode) { + print('Error DeepLinking: $e'); + } + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: ReversibleAppbar( + context, + titleText: 'افزایش اعتبار', + ), + body: Responsive(context).maxWidthInDesktop( + maxWidth: 800, + child: (contxet, maxWidth) => SingleChildScrollView( + physics: context.watch().state is PlansLoading + ? const NeverScrollableScrollPhysics() + : const BouncingScrollPhysics(), + child: Responsive(context).builder( + desktop: Column( + children: [ + BlocBuilder( + builder: (context, state) { + if (state is PlansSuccess) { + final plans = state.plans + ..add(Plans( + title: 'بسته ویژه سازمان‌ها', + image: '/paymant/6/cover.png')); + List> separatedPlans = []; + for (int i = 0; i < plans.length; i += 2) { + if (i + 1 < plans.length) { + separatedPlans.add([plans[i], plans[i + 1]]); + } else { + separatedPlans.add([plans[i]]); + } + } + + return ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: separatedPlans.length, + itemBuilder: (context, index) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate( + separatedPlans[index].length, + (innerIndex) { + final plan = + separatedPlans[index][innerIndex]; + return Expanded(child: SizedBox() + // PurchaseCard( + // plan: plan, + // button: plan.title == + // 'بسته ویژه سازمان‌ها' + // ? () => Padding( + // padding: + // const EdgeInsets.symmetric( + // vertical: 16.0), + // child: LoadingButton( + // onPressed: () async { + // await launchUrl( + // Uri.parse( + // 'tel:03132611885'), + // mode: LaunchMode + // .externalApplication) + // .onError( + // (error, stackTrace) { + // if (kDebugMode) { + // print( + // 'error open Link is: $error'); + // } + // return false; + // }, + // ); + // }, + // radius: 360, + // color: AppColors + // .green.defaultShade, + // width: MediaQuery.sizeOf( + // context) + // .width, + // child: Text( + // 'با تیم پشتیبانی تماس بگیرید', + // style: AppTextStyles.body4 + // .copyWith( + // color: + // Colors.white), + // )), + // ) + // : null, + // ), + ); + }, + ), + ); + }, + ); + } + if (state is PlansFail) { + return const SizedBox.shrink(); + } + + return ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: 6, + itemBuilder: (context, index) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate( + 2, + (innerIndex) { + return const Expanded( + child: PurchaseCardPlaceholder(), + ); + }, + ), + ); + }, + ); + }, + ), + ], + ), + mobile: Column( + children: [ + BlocBuilder( + builder: (context, state) { + if (state is PlansSuccess) { + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: state.plans.length, + itemBuilder: (context, index) { + final plan = state.plans[index]; + return PurchaseCard( + plan: plan, + ); + }); + } + if (state is PlansFail) { + return const SizedBox.shrink(); + } + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: 4, + itemBuilder: (context, index) { + return const PurchaseCardPlaceholder(); + }); + }, + ), + // PurchaseCard( + // plan: Plans( + // title: 'بسته ویژه سازمان‌ها', + // image: '/paymant/6/cover.png'), + // button: () => Padding( + // padding: const EdgeInsets.symmetric(vertical: 16.0), + // child: LoadingButton( + // onPressed: () async { + // await launchUrl(Uri.parse('tel:03132611885'), + // mode: LaunchMode.externalApplication) + // .onError( + // (error, stackTrace) { + // if (kDebugMode) { + // print('error open Link is: $error'); + // } + // return false; + // }, + // ); + // }, + // radius: 360, + // color: AppColors.green.defaultShade, + // width: MediaQuery.sizeOf(context).width, + // child: Text( + // 'با تیم پشتیبانی تماس بگیرید', + // style: AppTextStyles.body4 + // .copyWith(color: Colors.white), + // )), + // ), + // ), + ], + ), + )), + ), + ); + } +} diff --git a/lib/ui/screens/setting/bloc/paymant_history_bloc.dart b/lib/ui/screens/setting/bloc/paymant_history_bloc.dart new file mode 100644 index 0000000..ffd84a7 --- /dev/null +++ b/lib/ui/screens/setting/bloc/paymant_history_bloc.dart @@ -0,0 +1,42 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/billings_history_model.dart'; +import 'package:hoshan/data/repository/paymant_repository.dart'; + +part 'paymant_history_event.dart'; +part 'paymant_history_state.dart'; + +class PaymantHistoryBloc + extends Bloc { + PaymantHistoryBloc() : super(PaymantHistoryInitial()) { + on((event, emit) async { + if (event is GetAllHistory) { + emit(PaymantHistoryLoading()); + try { + final billings = await PaymantRepository.getPaymantHistory(); + if (billings.isEmpty) { + emit(PaymantHistoryEmpty()); + } else { + final List pBillings = []; + int count = 1; + for (var i = 0; i < billings.length; i += 5) { + pBillings.add(PaginationBillings( + page: count, + billings: billings.sublist( + i, i + 5 > billings.length ? billings.length : i + 5))); + count++; + } + emit(PaymantHistorySuccess(billings: pBillings)); + } + } on DioException catch (e) { + emit(PaymantHistoryFail()); + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } + }); + } +} diff --git a/lib/ui/screens/setting/bloc/paymant_history_event.dart b/lib/ui/screens/setting/bloc/paymant_history_event.dart new file mode 100644 index 0000000..b84a15c --- /dev/null +++ b/lib/ui/screens/setting/bloc/paymant_history_event.dart @@ -0,0 +1,10 @@ +part of 'paymant_history_bloc.dart'; + +sealed class PaymantHistoryEvent extends Equatable { + const PaymantHistoryEvent(); + + @override + List get props => []; +} + +class GetAllHistory extends PaymantHistoryEvent {} diff --git a/lib/ui/screens/setting/bloc/paymant_history_state.dart b/lib/ui/screens/setting/bloc/paymant_history_state.dart new file mode 100644 index 0000000..711684a --- /dev/null +++ b/lib/ui/screens/setting/bloc/paymant_history_state.dart @@ -0,0 +1,22 @@ +part of 'paymant_history_bloc.dart'; + +sealed class PaymantHistoryState extends Equatable { + const PaymantHistoryState(); + + @override + List get props => []; +} + +final class PaymantHistoryInitial extends PaymantHistoryState {} + +final class PaymantHistoryLoading extends PaymantHistoryState {} + +final class PaymantHistoryFail extends PaymantHistoryState {} + +final class PaymantHistoryEmpty extends PaymantHistoryState {} + +final class PaymantHistorySuccess extends PaymantHistoryState { + final List billings; + + const PaymantHistorySuccess({required this.billings}); +} diff --git a/lib/ui/screens/setting/bloc/report_of_use_bloc.dart b/lib/ui/screens/setting/bloc/report_of_use_bloc.dart new file mode 100644 index 0000000..e694166 --- /dev/null +++ b/lib/ui/screens/setting/bloc/report_of_use_bloc.dart @@ -0,0 +1,37 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/report_model.dart'; +import 'package:hoshan/data/repository/auth_repository.dart'; +import 'package:shamsi_date/shamsi_date.dart'; + +part 'report_of_use_event.dart'; +part 'report_of_use_state.dart'; + +class ReportOfUseBloc extends Bloc { + ReportOfUseBloc() : super(ReportOfUseInitial()) { + on((event, emit) async { + if (event is GetReport) { + emit(ReportOfUseLoading()); + try { + final response = + await AuthRepository.getReport(startDate: event.startDate); + if (response.report != null && response.report!.isEmpty) { + emit(ReportOfUseEmpty()); + } else { + emit(ReportOfUseSuccess(reportModel: response)); + } + } on DioException catch (e) { + emit(ReportOfUseFail()); + if (kDebugMode) { + print('Error is: $e'); + } + } + } + if (event is ClearReport) { + emit(ReportOfUseInitial()); + } + }); + } +} diff --git a/lib/ui/screens/setting/bloc/report_of_use_event.dart b/lib/ui/screens/setting/bloc/report_of_use_event.dart new file mode 100644 index 0000000..e8698e3 --- /dev/null +++ b/lib/ui/screens/setting/bloc/report_of_use_event.dart @@ -0,0 +1,16 @@ +part of 'report_of_use_bloc.dart'; + +sealed class ReportOfUseEvent extends Equatable { + const ReportOfUseEvent(); + + @override + List get props => []; +} + +class GetReport extends ReportOfUseEvent { + final Jalali? startDate; + + const GetReport({this.startDate}); +} + +class ClearReport extends ReportOfUseEvent {} diff --git a/lib/ui/screens/setting/bloc/report_of_use_state.dart b/lib/ui/screens/setting/bloc/report_of_use_state.dart new file mode 100644 index 0000000..ae3ca51 --- /dev/null +++ b/lib/ui/screens/setting/bloc/report_of_use_state.dart @@ -0,0 +1,22 @@ +part of 'report_of_use_bloc.dart'; + +sealed class ReportOfUseState extends Equatable { + const ReportOfUseState(); + + @override + List get props => []; +} + +final class ReportOfUseInitial extends ReportOfUseState {} + +final class ReportOfUseLoading extends ReportOfUseState {} + +final class ReportOfUseSuccess extends ReportOfUseState { + final ReportModel reportModel; + + const ReportOfUseSuccess({required this.reportModel}); +} + +final class ReportOfUseFail extends ReportOfUseState {} + +final class ReportOfUseEmpty extends ReportOfUseState {} diff --git a/lib/ui/screens/setting/cubit/ad_remaining_cubit.dart b/lib/ui/screens/setting/cubit/ad_remaining_cubit.dart new file mode 100644 index 0000000..47a66dc --- /dev/null +++ b/lib/ui/screens/setting/cubit/ad_remaining_cubit.dart @@ -0,0 +1,34 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/data/repository/auth_repository.dart'; + +part 'ad_remaining_state.dart'; + +class AdRemainingCubit extends Cubit { + AdRemainingCubit() : super(AdRemainingInitial()); + + void getRemainingAd() async { + emit(AdRemainingLoading()); + try { + final response = await DioService().sendRequest().get('/app-metadata'); + final Map parsed = { + for (var item in response.data) item['tag']: item['value'] + }; + final reminding = await AuthRepository.getRemaining(); + + parsed['remaining_seconds'] = + reminding.data['remaining_seconds'].toString(); + + emit(AdRemainingSuccess(data: parsed)); + } on DioException catch (e) { + emit(AdRemainingFail()); + + if (kDebugMode) { + print('Error fetching ad remaining: $e'); + } + } + } +} diff --git a/lib/ui/screens/setting/cubit/ad_remaining_state.dart b/lib/ui/screens/setting/cubit/ad_remaining_state.dart new file mode 100644 index 0000000..64c17c8 --- /dev/null +++ b/lib/ui/screens/setting/cubit/ad_remaining_state.dart @@ -0,0 +1,19 @@ +part of 'ad_remaining_cubit.dart'; + +sealed class AdRemainingState extends Equatable { + const AdRemainingState(); + + @override + List get props => []; +} + +final class AdRemainingInitial extends AdRemainingState {} + +final class AdRemainingLoading extends AdRemainingState {} + +final class AdRemainingSuccess extends AdRemainingState { + final Map data; + const AdRemainingSuccess({required this.data}); +} + +final class AdRemainingFail extends AdRemainingState {} diff --git a/lib/ui/screens/setting/cubit/check_username_cubit.dart b/lib/ui/screens/setting/cubit/check_username_cubit.dart new file mode 100644 index 0000000..10bd1a1 --- /dev/null +++ b/lib/ui/screens/setting/cubit/check_username_cubit.dart @@ -0,0 +1,37 @@ +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/data/repository/auth_repository.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; + +part 'check_username_state.dart'; + +class CheckUsernameCubit extends Cubit { + CheckUsernameCubit() : super(CheckUsernameInitial()); + + void loading() { + emit(CheckUsernameLoading()); + } + + Future check(String username) async { + emit(CheckUsernameLoading()); + if (username.length < 4) { + emit(CheckUsernameEmpty()); + return; + } + try { + if (username == UserInfoCubit.userInfoModel.username) { + emit(CheckUsernameSuccess()); + } else { + final response = await AuthRepository.checkUsernameIsValid(username); + emit(response ? CheckUsernameSuccess() : CheckUsernameFail()); + } + } on DioException catch (e) { + emit(CheckUsernameFail()); + if (kDebugMode) { + print('Dio Error: $e'); + } + } + } +} diff --git a/lib/ui/screens/setting/cubit/check_username_state.dart b/lib/ui/screens/setting/cubit/check_username_state.dart new file mode 100644 index 0000000..5bc1f89 --- /dev/null +++ b/lib/ui/screens/setting/cubit/check_username_state.dart @@ -0,0 +1,18 @@ +part of 'check_username_cubit.dart'; + +sealed class CheckUsernameState extends Equatable { + const CheckUsernameState(); + + @override + List get props => []; +} + +final class CheckUsernameInitial extends CheckUsernameState {} + +final class CheckUsernameSuccess extends CheckUsernameState {} + +final class CheckUsernameFail extends CheckUsernameState {} + +final class CheckUsernameLoading extends CheckUsernameState {} + +final class CheckUsernameEmpty extends CheckUsernameState {} diff --git a/lib/ui/screens/setting/cubit/report_pi_coin_cubit.dart b/lib/ui/screens/setting/cubit/report_pi_coin_cubit.dart new file mode 100644 index 0000000..19eb061 --- /dev/null +++ b/lib/ui/screens/setting/cubit/report_pi_coin_cubit.dart @@ -0,0 +1,31 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/report_model.dart'; +import 'package:hoshan/data/repository/auth_repository.dart'; +import 'package:shamsi_date/shamsi_date.dart'; + +part 'report_pi_coin_state.dart'; + +class ReportPiCoinCubit extends Cubit { + ReportPiCoinCubit() : super(ReportPiCoinInitial()); + + Future getReport({final Jalali? startDate}) async { + emit(ReportPiCoinLoading()); + try { + final response = await AuthRepository.getReportCoin(startDate: startDate); + + if (response.report != null && response.report!.isNotEmpty) { + emit(ReportPiCoinSuccess(points: response.report!)); + } else { + emit(ReportPiCoinEmpty()); + } + } on DioException catch (e) { + emit(ReportPiCoinFail()); + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } +} diff --git a/lib/ui/screens/setting/cubit/report_pi_coin_state.dart b/lib/ui/screens/setting/cubit/report_pi_coin_state.dart new file mode 100644 index 0000000..6b3ab4f --- /dev/null +++ b/lib/ui/screens/setting/cubit/report_pi_coin_state.dart @@ -0,0 +1,22 @@ +part of 'report_pi_coin_cubit.dart'; + +sealed class ReportPiCoinState extends Equatable { + const ReportPiCoinState(); + + @override + List get props => []; +} + +final class ReportPiCoinInitial extends ReportPiCoinState {} + +final class ReportPiCoinLoading extends ReportPiCoinState {} + +final class ReportPiCoinSuccess extends ReportPiCoinState { + final List points; + + const ReportPiCoinSuccess({required this.points}); +} + +final class ReportPiCoinEmpty extends ReportPiCoinState {} + +final class ReportPiCoinFail extends ReportPiCoinState {} diff --git a/lib/ui/screens/setting/cubit/settlement_cubit.dart b/lib/ui/screens/setting/cubit/settlement_cubit.dart new file mode 100644 index 0000000..790c5b8 --- /dev/null +++ b/lib/ui/screens/setting/cubit/settlement_cubit.dart @@ -0,0 +1,24 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/repository/paymant_repository.dart'; + +part 'settlement_state.dart'; + +class SettlementCubit extends Cubit { + SettlementCubit() : super(SettlementInitial()); + + void init(bool isRial) async { + emit(SettlementLoading()); + try { + final message = await PaymantRepository.setSettlement(isRial); + emit(SettlementSuccess(message: message)); + } on DioException catch (e) { + emit(SettlementFail(message: e.response?.data['detail'])); + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } +} diff --git a/lib/ui/screens/setting/cubit/settlement_state.dart b/lib/ui/screens/setting/cubit/settlement_state.dart new file mode 100644 index 0000000..5523dcc --- /dev/null +++ b/lib/ui/screens/setting/cubit/settlement_state.dart @@ -0,0 +1,24 @@ +part of 'settlement_cubit.dart'; + +sealed class SettlementState extends Equatable { + const SettlementState(); + + @override + List get props => []; +} + +final class SettlementInitial extends SettlementState {} + +final class SettlementLoading extends SettlementState {} + +final class SettlementSuccess extends SettlementState { + final String message; + + const SettlementSuccess({required this.message}); +} + +final class SettlementFail extends SettlementState { + final String? message; + + const SettlementFail({this.message}); +} diff --git a/lib/ui/screens/setting/edit_profile_page.dart b/lib/ui/screens/setting/edit_profile_page.dart new file mode 100644 index 0000000..272c3d2 --- /dev/null +++ b/lib/ui/screens/setting/edit_profile_page.dart @@ -0,0 +1,764 @@ +// ignore_for_file: deprecated_member_use_from_same_package, use_build_context_synchronously + +import 'package:cross_file/cross_file.dart'; +import 'package:dio/dio.dart'; +import 'package:easy_debounce/easy_debounce.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/data/model/ai/credit_model.dart'; +import 'package:hoshan/data/repository/auth_repository.dart'; +import 'package:hoshan/ui/screens/setting/cubit/check_username_cubit.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/animations/animated_visibility.dart'; +import 'package:hoshan/ui/widgets/components/button/circle_icon_btn.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/dialog/bottom_sheets.dart'; +import 'package:hoshan/ui/widgets/components/dialog/dialog_handler.dart'; +import 'package:hoshan/ui/widgets/components/image/custome_image.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; +import 'package:hoshan/ui/widgets/components/text/auth_text_field.dart'; +import 'package:hoshan/ui/widgets/components/text/card_number_input.dart'; +import 'package:hoshan/ui/widgets/sections/header/reversible_appbar.dart'; + +class EditProfilePage extends StatefulWidget { + const EditProfilePage({super.key}); + + @override + State createState() => _EditProfilePageState(); +} + +class _EditProfilePageState extends State { + TextEditingController username = TextEditingController(); + TextEditingController mobile = TextEditingController(); + TextEditingController email = TextEditingController(); + TextEditingController pasword = TextEditingController(); + TextEditingController rePassword = TextEditingController(); + String? imageUrl; + String cardNumber = ''; + final ValueNotifier image = ValueNotifier(null); + final ValueNotifier loading = ValueNotifier(false); + final ValueNotifier showCard = ValueNotifier(false); + + @override + void initState() { + imageUrl = UserInfoCubit.userInfoModel.image; + + if (UserInfoCubit.userInfoModel.cardNumber != null) { + cardNumber = UserInfoCubit.userInfoModel.cardNumber!; + showCard.value = true; + } + if (UserInfoCubit.userInfoModel.username != null) { + username.text = UserInfoCubit.userInfoModel.username!; + } + if (UserInfoCubit.userInfoModel.mobileNumber != null) { + mobile.text = UserInfoCubit.userInfoModel.mobileNumber!; + } + if (UserInfoCubit.userInfoModel.email != null) { + email.text = UserInfoCubit.userInfoModel.email!; + } + super.initState(); + } + + @override + dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: ReversibleAppbar( + context, + titleText: 'ویرایش پروفایل', + ), + body: Responsive(context).maxWidthInDesktop( + child: (contxet, maxWidth) => Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: BlocConsumer( + listener: (context, state) {}, + builder: (context, state) { + return Column( + children: [ + Center( + child: Stack( + children: [ + ValueListenableBuilder( + valueListenable: image, + builder: (context, date, _) { + return Container( + width: 140, + height: 140, + margin: const EdgeInsets.only( + top: 24, bottom: 40), + decoration: BoxDecoration( + color: AppColors.gray[300], + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + blurRadius: 6, + color: const Color(0xff4d4d4d) + .withValues(alpha: 0.4)) + ]), + child: ClipOval( + child: date != null + ? GestureDetector( + onTap: () => DialogHandler( + context: context) + .showImageHero( + image: date.path), + child: CustomeImage( + src: date.path, + fit: BoxFit.cover, + ), + ) + : imageUrl != null && + imageUrl!.isNotEmpty + ? ImageNetwork( + url: DioService.baseURL + + imageUrl!, + showHero: true, + ) + : Padding( + padding: + const EdgeInsets.all( + 32), + child: Assets + .icon.bold.profile + .svg(), + ), + ), + ); + }), + Positioned( + bottom: 0 + 40, + left: 0, + child: CircleIconBtn( + icon: Assets.icon.outline.galleryAdd, + color: AppColors.secondryColor.defaultShade, + iconColor: Colors.white, + size: 48, + iconPadding: const EdgeInsets.all(10), + onTap: () => + BottomSheetHandler(context).showPickImage( + withAvatar: true, + profile: true, + onSelect: (file) { + image.value = file; + }, + ), + ), + ) + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Directionality( + textDirection: TextDirection.rtl, + child: Column( + children: [ + BlocBuilder( + builder: (context, state) { + return Column( + children: [ + AuthTextField( + justEnglish: true, + success: state is CheckUsernameSuccess + ? Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Icon( + Icons.check_circle, + color: AppColors + .green.defaultShade, + size: 16, + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + 'نام کاربری در دسترس است', + style: AppTextStyles + .body5 + .copyWith( + color: AppColors + .green + .defaultShade), + ), + ), + ], + ) + : null, + error: state is CheckUsernameFail || + state is CheckUsernameEmpty + ? Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Icon( + Icons + .warning_amber_rounded, + color: AppColors + .red.defaultShade, + size: 16, + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + state is CheckUsernameFail + ? 'نام کاربری قبلا انتخاب شده است' + : 'نام کاربری کوتاه است', + style: AppTextStyles + .body5 + .copyWith( + color: AppColors + .red + .defaultShade), + ), + ), + ], + ) + : null, + label: 'نام کاربری', + suffix: Padding( + padding: const EdgeInsets.all(8.0), + child: Assets + .icon.outline.profileTick + .svg( + color: state + is CheckUsernameFail + ? AppColors + .red.defaultShade + : state + is CheckUsernameSuccess + ? AppColors.green + .defaultShade + : Theme.of(context) + .colorScheme + .primary), + ), + controller: username, + onChange: (usernameText) { + context + .read() + .loading(); + EasyDebounce.debounce('my-username', + const Duration(seconds: 1), () { + context + .read() + .check(usernameText); + }); + }, + ), + if (state is CheckUsernameLoading) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 18, vertical: 4), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SizedBox( + width: 16, + height: 16, + child: + CircularProgressIndicator( + color: Theme.of(context) + .colorScheme + .primary, + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + 'درحال بررسی', + style: AppTextStyles.body5 + .copyWith( + color: Theme.of( + context) + .colorScheme + .primary), + ), + ), + ], + ), + ) + ], + ); + }, + ), + + if (mobile.text.isNotEmpty) + Column( + children: [ + const SizedBox( + height: 24, + ), + AuthTextField( + label: 'تلفن همراه', + enabled: false, + suffix: Padding( + padding: const EdgeInsets.all(10.0), + child: Assets.icon.outline.call.svg( + color: Theme.of(context) + .colorScheme + .primary), + ), + controller: mobile, + ), + ], + ), + if (email.text.isNotEmpty) + Column( + children: [ + const SizedBox( + height: 24, + ), + AuthTextField( + label: 'ایمیل', + enabled: false, + suffix: Padding( + padding: const EdgeInsets.all(10.0), + child: Icon(Icons.email_rounded, + color: Theme.of(context) + .colorScheme + .primary)), + controller: email, + ), + ], + ), + // const SizedBox( + // height: 24, + // ), + // AuthTextField( + // label: 'آدرس ایمیل', + // suffix: Padding( + // padding: const EdgeInsets.all(10.0), + // child: Assets.icon.outline.smsTracking.svg(), + // ), + // controller: email, + // ), + const SizedBox( + height: 24, + ), + ValueListenableBuilder( + valueListenable: rePassword, + builder: (context, rePass, child) { + return ValueListenableBuilder( + valueListenable: pasword, + builder: (context, pass, child) { + return Column( + children: [ + // AuthTextField( + // label: 'رمز عبور', + // textInputAction: + // TextInputAction.next, + // suffix: Padding( + // padding: const EdgeInsets.all( + // 10.0), + // child: Assets + // .icon.outline.lock + // .svg( + // color: pass.text != + // rePass.text + // ? AppColors.red + // .defaultShade + // : Theme.of( + // context) + // .colorScheme + // .primary), + // ), + // controller: pasword, + // error: pass.text.length < 8 && + // pass.text.isNotEmpty + // ? Row( + // crossAxisAlignment: + // CrossAxisAlignment + // .start, + // children: [ + // Icon( + // Icons + // .warning_amber_rounded, + // color: AppColors.red + // .defaultShade, + // size: 16, + // ), + // const SizedBox( + // width: 8, + // ), + // Expanded( + // child: Text( + // 'رمز عبور حداقل باید ۸ کارکتر باشد', + // style: AppTextStyles + // .body5 + // .copyWith( + // color: AppColors + // .red + // .defaultShade), + // ), + // ), + // ], + // ) + // : pass.text != rePass.text + // ? const SizedBox() + // : null, + // ), + // const SizedBox( + // height: 24, + // ), + // ValueListenableBuilder( + // valueListenable: rePassword, + // builder: + // (context, pass, child) { + // return AuthTextField( + // label: 'تکرار رمز عبور', + // suffix: Padding( + // padding: + // const EdgeInsets.all( + // 10.0), + // child: Assets + // .icon.outline.lock + // .svg( + // color: pasword + // .text != + // pass.text + // ? AppColors + // .red + // .defaultShade + // : Theme.of( + // context) + // .colorScheme + // .primary), + // ), + // controller: rePassword, + // error: + // pasword.text != + // pass.text + // ? Row( + // crossAxisAlignment: + // CrossAxisAlignment + // .start, + // children: [ + // Icon( + // Icons + // .warning_amber_rounded, + // color: AppColors + // .red + // .defaultShade, + // size: 16, + // ), + // const SizedBox( + // width: 8, + // ), + // Expanded( + // child: Text( + // 'رمز عبور با تکرار آن مطابقت ندارد', + // style: AppTextStyles + // .body5 + // .copyWith( + // color: AppColors.red.defaultShade), + // ), + // ), + // ], + // ) + // : null, + // ); + // }, + // ), + ], + ); + }, + ); + }), + + const SizedBox( + height: 24, + ), + ValueListenableBuilder( + valueListenable: showCard, + builder: (context, show, child) { + return Column( + children: [ + // GestureDetector( + // onTap: () => showCard.value = !show, + // child: Row( + // children: [ + // Transform.scale( + // scale: 1.4, + // child: Checkbox( + // value: show, + // activeColor: AppColors + // .secondryColor + // .defaultShade, + // side: BorderSide( + // width: 1, + // color: Theme.of(context) + // .colorScheme + // .onSurface), + // onChanged: (value) { + // showCard.value = + // value ?? false; + // }, + // materialTapTargetSize: + // MaterialTapTargetSize + // .shrinkWrap, + // shape: + // RoundedRectangleBorder( + // borderRadius: + // BorderRadius + // .circular( + // 6)), + // ), + // ), + // // Expanded( + // // child: Text( + // // 'تمایل به کسب درآمد از طریق ساخت دستیار دارم.', + // // style: AppTextStyles.body4 + // // .copyWith( + // // color: Theme.of( + // // context) + // // .colorScheme + // // .onSurface), + // // ), + // // ), + // ], + // ), + // ), + AnimatedVisibility( + isVisible: show, + duration: const Duration( + milliseconds: 300), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 24, + ), + Text( + 'وارد کردن شماره کارت برای دریافت درآمد:', + style: AppTextStyles.body4 + .copyWith( + color: Theme.of( + context) + .colorScheme + .onSurface), + ), + Padding( + padding: const EdgeInsets + .symmetric( + vertical: 8.0), + child: CardNumberInput( + initialValue: cardNumber, + onChange: (value) { + cardNumber = value; + }, + ), + ), + Row( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Icon( + Icons + .warning_amber_rounded, + color: AppColors + .red.defaultShade, + size: 32, + ), + const SizedBox( + width: 4, + ), + Expanded( + child: Text( + 'شماره کارت شما برای انتقال درآمد ماهانه استفاده می‌شود. حریم خصوصی شما تضمین شده است و اطلاعات شما کاملا محرمانه خواهد بود.', + textAlign: + TextAlign.justify, + style: AppTextStyles + .body5 + .copyWith( + color: AppColors + .black[ + 300]), + ), + ) + ], + ) + ], + )) + ], + ); + }), + const SizedBox( + height: 40, + ), + ], + ), + ), + ) + ], + ); + }, + ), + ), + ), + Container( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), + color: Theme.of(context).scaffoldBackgroundColor, + child: ValueListenableBuilder( + valueListenable: loading, + builder: (context, data, _) { + return LoadingButton( + width: MediaQuery.sizeOf(context).width, + height: 40, + color: AppColors.primaryColor.defaultShade, + loading: data, + radius: 24, + onPressed: data + ? null + : () async { + try { + loading.value = true; + + if (username.text != + UserInfoCubit.userInfoModel.username && + (context.read().state + is CheckUsernameSuccess)) { + try { + await AuthRepository.editUsername( + username.text); + } on DioException catch (e) { + SnackBarManager(context, + id: 'changeUsername') + .show( + status: SnackBarStatus.error, + message: + 'خطا در تغییر نام کاربری دوباره سعی کنید'); + Future.delayed(const Duration(seconds: 3)); + if (kDebugMode) { + print('Error is: $e'); + } + } + } + if (pasword.text.isNotEmpty) { + if (pasword.text == rePassword.text) { + try { + await AuthRepository.editPasswordProfile( + pasword.text); + } on DioException catch (e) { + SnackBarManager(context, + id: 'changePassword') + .show( + status: SnackBarStatus.error, + message: + 'خطا در تغییر رمز دوباره سعی کنید'); + Future.delayed( + const Duration(seconds: 3)); + if (kDebugMode) { + print('Error is: $e'); + } + } + } + } + if (image.value != null) { + try { + await AuthRepository.editImageProfile( + image.value!); + } on DioException catch (e) { + SnackBarManager(context, id: 'changeImage') + .show( + status: SnackBarStatus.error, + message: + 'خطا در آپلود فایل دوباره سعی کنید'); + Future.delayed(const Duration(seconds: 3)); + if (kDebugMode) { + print('Error is: $e'); + } + } + } + + // if (cardNumber != + // UserInfoCubit.userInfoModel.cardNumber) { + // if (cardNumber.length == 16 || + // cardNumber.isEmpty) { + try { + await AuthRepository.editCardNumber( + showCard.value ? cardNumber : ''); + } on DioException catch (e) { + SnackBarManager(context, id: 'changeCard') + .show( + status: SnackBarStatus.error, + message: + 'خطا در ثبت شماره کارت دوباره سعی کنید', + ); + Future.delayed(const Duration(seconds: 3)); + if (kDebugMode) { + print('Error is: $e'); + } + } + // } + // } + + try { + await context + .read() + .getUserInfo() + .then( + (value) { + SnackBarManager(context).show( + status: SnackBarStatus.success, + message: + 'اطلاعات با موفقیت بروزرسانی شد'); + }, + ); + context.read().changeCredit( + CreditModel( + credit: UserInfoCubit + .userInfoModel.credit, + freeCredit: UserInfoCubit + .userInfoModel.freeCredit)); + } catch (e) { + if (kDebugMode) { + print('Error is: $e'); + } + } + } catch (e) { + if (kDebugMode) { + print('Error is: $e'); + } + } + loading.value = false; + }, + child: Text( + 'تایید', + style: + AppTextStyles.body4.copyWith(color: Colors.white), + ), + ); + }), + ) + ], + ), + ), + ); + } +} diff --git a/lib/ui/screens/setting/income_page.dart b/lib/ui/screens/setting/income_page.dart new file mode 100644 index 0000000..d59ab60 --- /dev/null +++ b/lib/ui/screens/setting/income_page.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/dialog/bottom_sheets.dart'; +import 'package:hoshan/ui/widgets/sections/header/reversible_appbar.dart'; + +class IncomePage extends StatelessWidget { + const IncomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: ReversibleAppbar( + context, + titleText: 'راهنمای فعال سازی درآمد', + ), + body: Responsive(context).maxWidthInDesktop( + child: (contxet, maxWidth) => SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Directionality( + textDirection: TextDirection.rtl, + child: Theme( + data: Theme.of(context).copyWith( + listTileTheme: Theme.of(context).listTileTheme.copyWith( + subtitleTextStyle: AppTextStyles.body4.copyWith( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900]), + titleTextStyle: AppTextStyles.body3.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary))), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + child: Column( + children: [ + const ListTile( + title: Text('راهنمای ساخت دستیار و کسب درآمد:'), + subtitle: Text( + 'آیا می‌دانستید می‌توانید با ساخت یک دستیار یا بات هوش مصنوعی و به اشتراک گذاشتن آن با دیگر کاربران، به درآمد برسید؟', + textAlign: TextAlign.justify, + ), + ), + const SizedBox( + height: 8, + ), + const ListTile( + title: Text('نحوه کار:'), + subtitle: Text( + '1. دستیار یا بات هوش مصنوعی موردنظر خود را طراحی و فعال کنید.\n2. کاربران دیگر بسته به نیاز خود، از دستیار شما استفاده خواهند کرد.\n3. بر اساس فرمول خاصی درآمد شما محاسبه خواهد شد.\n4. درآمد شما در پایان هر ماه به‌صورت نقدی به کارت بانکی شما واریز می‌شود. ', + textAlign: TextAlign.justify, + ), + ), + const SizedBox( + height: 16, + ), + LoadingButton( + onPressed: () async { + await BottomSheetHandler(context).showIncomeFormula(); + }, + color: AppColors.primaryColor.defaultShade, + height: 48, + radius: 12, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Assets.icon.outline.emptyWallet.svg(), + const SizedBox( + width: 8, + ), + Text( + 'فرمول محاسبه درآمد از طریق ساخت دستیار', + style: AppTextStyles.body4 + .copyWith(color: Colors.white), + ), + ], + )), + const SizedBox( + height: 16, + ), + const ListTile( + title: Text('چرا این فرصت عالی است؟'), + subtitle: Text( + '\u2022 مهارت خود را در ساخت بات‌های هوشمند نشان دهید.\n\u2022 به دیگران کمک کنید تجربه بهتری با هوش مصنوعی داشته باشند.\n\u2022 یک منبع درآمد منظم و جذاب برای خود ایجاد کنید.', + textAlign: TextAlign.justify, + ), + ), + const SizedBox( + height: 8, + ), + const ListTile( + title: Text('تکمیل مشخصات برای دریافت درآمد:'), + subtitle: Text( + 'برای اینکه بتوانیم درآمد شما را به‌موقع واریز کنیم، لازم است مشخصات خود را تکمیل کنید. این کار فقط چند دقیقه زمان می‌برد!', + textAlign: TextAlign.justify, + ), + ), + const SizedBox( + height: 8, + ), + const Divider(), + const SizedBox( + height: 8, + ), + Assets.image.incomeSteps.image( + width: Responsive(context).isMobile() + ? MediaQuery.sizeOf(context).width + : 500, + fit: BoxFit.cover), + const SizedBox( + height: 8, + ), + const Divider(), + const SizedBox( + height: 8, + ), + Text( + 'منتظر چی هستید؟ همین حالا شروع کنید،\nدستیار هوش مصنوعی بسازید و درآمد کسب کنید!', + style: AppTextStyles.body4.copyWith( + color: AppColors.red.defaultShade, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ) + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/ui/screens/setting/my_account_page.dart b/lib/ui/screens/setting/my_account_page.dart new file mode 100644 index 0000000..1d00b81 --- /dev/null +++ b/lib/ui/screens/setting/my_account_page.dart @@ -0,0 +1,829 @@ +// ignore_for_file: deprecated_member_use_from_same_package, use_build_context_synchronously + +import 'dart:math'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/services/file_manager/excel_services.dart'; +import 'package:hoshan/core/utils/date_time.dart'; +import 'package:hoshan/data/model/ai/credit_model.dart'; +import 'package:hoshan/data/model/empty_states_enum.dart'; +import 'package:hoshan/ui/screens/setting/bloc/paymant_history_bloc.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/circle_icon_btn.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/dropdown/animated_setting_container.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; +import 'package:hoshan/ui/widgets/sections/empty/empty_states.dart'; +import 'package:hoshan/ui/widgets/sections/header/reversible_appbar.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; +import 'package:open_filex/open_filex.dart'; +import 'package:persian_number_utility/persian_number_utility.dart'; + +class MyAccountPage extends StatefulWidget { + const MyAccountPage({super.key}); + + @override + State createState() => _MyAccountPageState(); +} + +class _MyAccountPageState extends State { + List dataColumns = [ + DataColumn( + label: Text('ردیف', + style: AppTextStyles.body4.copyWith(color: AppColors.gray[700]))), + DataColumn( + label: Text('مبلغ', + style: AppTextStyles.body4.copyWith(color: AppColors.gray[700]))), + DataColumn( + label: Text('بابت', + style: AppTextStyles.body4.copyWith(color: AppColors.gray[700]))), + DataColumn( + label: Text('تاریخ', + style: AppTextStyles.body4.copyWith(color: AppColors.gray[700]))), + DataColumn( + label: Text('وضعیت', + style: AppTextStyles.body4.copyWith(color: AppColors.gray[700]))), + DataColumn( + label: Text('کدپیگیری', + style: AppTextStyles.body4.copyWith(color: AppColors.gray[700]))), + ]; + List> dataRows = []; + + late final style = AppTextStyles.body4.copyWith( + color: + AppColors.gray[context.read().isDark() ? 600 : 900]); + + ScrollController scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + + context.read().changeCredit(CreditModel( + credit: UserInfoCubit.userInfoModel.credit ?? 0, + freeCredit: UserInfoCubit.userInfoModel.freeCredit ?? 0)); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: ReversibleAppbar( + context, + titleText: 'حساب من', + ), + body: Responsive(context).builder( + desktop: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox( + height: 24, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: history(context)), + Expanded( + child: Column( + children: [infoes(), faqs(context)], + ), + ), + ], + ), + ], + ), + ), + mobile: SingleChildScrollView( + child: Column( + children: [ + infoes(), + // Container( + // padding: const EdgeInsets.all(12), + // margin: const EdgeInsets.symmetric(horizontal: 16), + // decoration: BoxDecoration( + // color: Colors.white, + // borderRadius: BorderRadius.circular(16), + // ), + // child: Directionality( + // textDirection: TextDirection.rtl, + // child: Row( + // children: [ + // CircleIconBtn( + // icon: Assets.icon.bulk.gift, + // size: 80, + // iconPadding: const EdgeInsets.all(12), + // color: AppColors.primaryColor.defaultShade, + // ), + // const SizedBox( + // width: 16, + // ), + // Expanded( + // child: Column( + // mainAxisAlignment: MainAxisAlignment.start, + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Text( + // 'امتیاز وفاداری', + // style: AppTextStyles.body3.copyWith( + // color: AppColors.primaryColor.defaultShade, + // fontWeight: FontWeight.bold), + // ), + // const SizedBox( + // height: 8, + // ), + // Text( + // 'معادل 10% تخفیف در خرید بعدی و یا شارژ 200 توکن برای ابزار تولید عکس', + // style: AppTextStyles.body4.copyWith( + // color: AppColors.black.defaultShade), + // ), + // const SizedBox( + // height: 8, + // ), + // Padding( + // padding: const EdgeInsets.only(left: 16.0), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.end, + // children: [ + // Container( + // padding: + // const EdgeInsets.fromLTRB(12, 4, 8, 4), + // decoration: BoxDecoration( + // color: + // AppColors.primaryColor.defaultShade, + // borderRadius: const BorderRadius.only( + // bottomRight: Radius.circular(4), + // topRight: Radius.circular(4))), + // child: Row( + // children: [ + // Assets.icon.outline.documentCopy + // .svg(color: Colors.white), + // const SizedBox( + // width: 2, + // ), + // Text( + // 'کپی', + // style: AppTextStyles.body4 + // .copyWith(color: Colors.white), + // ), + // ], + // ), + // ), + // Container( + // padding: + // const EdgeInsets.fromLTRB(8, 4, 12, 4), + // decoration: BoxDecoration( + // color: AppColors.gray[200], + // borderRadius: const BorderRadius.only( + // bottomLeft: Radius.circular(4), + // topLeft: Radius.circular(4))), + // child: Text( + // '2AZ5H', + // style: AppTextStyles.body4 + // .copyWith(color: AppColors.gray[700]), + // ), + // ), + // ], + // ), + // ) + // ], + // ), + // ), + + // ], + // ), + // ), + // ), + // const SizedBox( + // height: 16, + // ), + history(context), + + faqs(context) + ], + ), + ), + ), + ); + } + + Padding faqs(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 24, bottom: 46), + child: Column( + children: [ + AnimatedSettingContainer( + title: 'سکه‌ها توی این اپلیکیشن چی هستن؟', + icon: Icons.filter_1_rounded, + childrens: [ + const SizedBox( + height: 24, + ), + Text( + 'سکه‌ها ارزهای مجازی داخل اپلیکیشن هوشان هستند که می‌تونی باهاشون قابلیت‌های ویژه‌ای رو فعال کنی. مثلاً ممکنه برای استفاده از امکانات پیشرفته‌تر یا درخواست‌های بیشتر، نیاز به خرج کردن سکه داشته باشی.', + textDirection: TextDirection.rtl, + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[ + context.read().isDark() ? 600 : 900]), + ), + const SizedBox( + height: 12, + ), + ]), + AnimatedSettingContainer( + title: 'چطوری می‌تونم سکه به دست بیارم؟', + icon: Icons.filter_2_rounded, + childrens: [ + const SizedBox( + height: 24, + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + 'راه‌های مختلفی وجود داره:\n', + textDirection: TextDirection.rtl, + style: style, + ), + ], + ), + Directionality( + textDirection: TextDirection.rtl, + child: Text.rich(TextSpan(style: style, children: [ + TextSpan( + text: 'شارژ حساب: ', + style: style.copyWith(fontWeight: FontWeight.bold)), + TextSpan( + text: + 'با ارتقای حساب کاربری، می‌تونی به میزان مصرفت، سکه بدست بیاری.\n\n', + style: style), + TextSpan( + text: 'کیف پول باسا: ', + style: style.copyWith(fontWeight: FontWeight.bold)), + TextSpan( + text: + 'در بازه های زمانی مختلف کیف پول باسا شما شارژ میشه و میتونید از اعتبار آن استفاده کنید.', + style: style), + // TextSpan( + // text: 'هدیه روزانه: ', + // style: style.copyWith(fontWeight: FontWeight.bold)), + // TextSpan( + // text: + // 'هوشان هر روز 10 عدد سکه رایگان به عنوان هدیه بهت میده.', + // style: style), + ])), + ), + const SizedBox( + height: 12, + ), + ]), + // AnimatedSettingContainer( + // title: + // 'اگر از سکه‌های هدیه روزانه هوشان استفاده نکنم، در پایان روز از بین می‌روند؟', + // icon: Icons.filter_3_rounded, + // childrens: [ + // const SizedBox( + // height: 24, + // ), + // Text( + // 'سکه‌ای که هر روز از هوشان دریافت می‌کنی، فقط تا پایان همون روز اعتبار داره. اگه ازش استفاده نکنی، منقضی می‌شه و به روز بعد منتقل نمی‌شه. پس بهتره قبل از تموم شدن روز ازش استفاده کنی!', + // textDirection: TextDirection.rtl, + // style: AppTextStyles.body4.copyWith( + // color: AppColors.gray[ + // context.read().isDark() ? 600 : 900]), + // ), + // const SizedBox( + // height: 12, + // ), + // ]), + // AnimatedSettingContainer( + // title: + // 'اگر سکه‌هام تموم بشه، دیگه نمی‌تونم از اپلیکیشن استفاده کنم؟', + // icon: Icons.filter_3_rounded, + // childrens: [ + // const SizedBox( + // height: 24, + // ), + // Text( + // 'چرا میتونی ! برخی از امکانات پایه‌ای اپلیکیشن رایگان هستن. ولی برای دریافت امکانات پیشرفته یا درخواست‌های بیشتر باید سکه تهیه کنی. می‌تونی حسابت رو ارتقا بدی یا صبر کنی تا سکه رایگان روزانه دریافت کنی.', + // textDirection: TextDirection.rtl, + // style: AppTextStyles.body4.copyWith( + // color: AppColors.gray[ + // context.read().isDark() ? 600 : 900]), + // ), + // const SizedBox( + // height: 12, + // ), + // ]), + // AnimatedSettingContainer( + // title: 'چطور می‌تونم از طریق هوشان کسب درآمد کنم؟', + // icon: Icons.filter_4_rounded, + // childrens: [ + // const SizedBox( + // height: 24, + // ), + // Text( + // 'در قسمت دستیارها، می‌تونی یک دستیار کاربردی بسازی و اون رو با دیگران به اشتراک بذاری. با هر بار استفاده از دستیارت، تو می‌تونی به درآمد برسی.', + // textDirection: TextDirection.rtl, + // style: AppTextStyles.body4.copyWith( + // color: AppColors.gray[ + // context.read().isDark() ? 600 : 900]), + // ), + // const SizedBox( + // height: 12, + // ), + // ]), + ], + ), + ); + } + + Column history(BuildContext context) { + return Column( + children: [ + Padding( + padding: EdgeInsets.symmetric( + horizontal: Responsive(context).isMobile() ? 0 : 90.0), + child: Directionality( + textDirection: TextDirection.rtl, + child: ListTile( + title: Text( + 'تاریخچه تراکنش‌ها', + style: + AppTextStyles.body3.copyWith(fontWeight: FontWeight.bold), + ), + trailing: context.watch().state + is PaymantHistorySuccess + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + CircleIconBtn( + color: context.read().isDark() + ? AppColors.black[900] + : AppColors.primaryColor[50], + iconColor: Theme.of(context).colorScheme.primary, + onTap: () { + if (context.read().state + is PaymantHistorySuccess) { + final dc = [...dataColumns]; + List dr = []; + for (var d in dataRows) { + dr.addAll(d); + } + dr = dr.map( + (e) { + final row = e; + row.cells.removeAt(4); + return row; + }, + ).toList(); + dc.removeAt(4); + + ExcelServices.saveDataTableToExcel(dc, dr).then( + (path) async { + if (path == null) { + SnackBarManager(context).show( + status: SnackBarStatus.error, + message: 'خطا در ذخیره سازی فایل'); + } else { + try { + if (!kIsWeb) { + await ExcelServices.copyToDownloads( + path, path.split('/').last); + } + + SnackBarManager(context).show( + status: SnackBarStatus.success, + message: 'فایل با موفقیت دانلود شد', + btns: GestureDetector( + onTap: () async { + try { + await OpenFilex.open(path); + } catch (e) { + if (kDebugMode) { + print(e); + } + } + }, + child: Text( + 'باز کردن فایل', + style: AppTextStyles.body5.copyWith( + color: AppColors + .black.defaultShade), + ), + ), + backgroundColor: AppColors.green[50], + ); + } catch (e) { + SnackBarManager(context).show( + status: SnackBarStatus.error, + message: 'خطا در ذخیره سازی فایل'); + } + } + }, + ); + } + }, + icon: Assets.icon.outline.download, + ), + const SizedBox( + width: 8, + ), + CircleIconBtn( + color: context.read().isDark() + ? AppColors.black[900] + : AppColors.primaryColor[50], + iconColor: Theme.of(context).colorScheme.primary, + onTap: () => context + .read() + .add(GetAllHistory()), + icon: Assets.icon.outline.bitcoinRefresh, + ), + const SizedBox( + width: 8, + ), + CircleIconBtn( + color: context.read().isDark() + ? AppColors.black[900] + : AppColors.primaryColor[50], + iconColor: Theme.of(context).colorScheme.primary, + onTap: () { + try { + scrollController.animateTo( + scrollController.position.minScrollExtent, + duration: 600.ms, + curve: Curves.easeIn); + } catch (e) { + if (kDebugMode) { + print('Error is: $e'); + } + } + }, + icon: Assets.icon.outline.arrowFlashRight), + const SizedBox( + width: 4, + ), + Transform.rotate( + angle: pi, + child: CircleIconBtn( + color: context.read().isDark() + ? AppColors.black[900] + : AppColors.primaryColor[50], + iconColor: + Theme.of(context).colorScheme.primary, + onTap: () { + try { + scrollController.animateTo( + scrollController + .position.maxScrollExtent, + duration: 600.ms, + curve: Curves.easeIn); + } catch (e) { + if (kDebugMode) { + print('Error is: $e'); + } + } + }, + icon: Assets.icon.outline.arrowFlashRight)), + ], + ) + : null, + ), + ), + ), + BlocBuilder( + builder: (context, state) { + if (state is PaymantHistorySuccess) { + ValueNotifier index = ValueNotifier(0); + dataRows = List.generate( + state.billings.length, + (page) => List.generate( + state.billings[page].billings.length, + (i) { + final billing = state.billings[page].billings[i]; + return DataRow( + color: WidgetStateProperty.resolveWith( + (Set states) { + if (context.read().state == + ThemeMode.dark) { + return (i % 2 == 0) + ? AppColors.gray[900] + : Theme.of(context).colorScheme.surface; + } else { + return (i % 2 == 0) + ? AppColors.gray[200] + : Colors.white; + } + }, + ), + cells: [ + DataCell(Text(((page * 5) + (i + 1)).toString(), + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface))), + DataCell(Text( + '${billing.amount.toString().seRagham()} تومان', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface))), + // DataCell(Text(billing.amount.toString(), + // style: AppTextStyles.body4)), + + DataCell(Text('${billing.credit} سکه', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface))), + DataCell(Text( + DateTimeUtils.convertStringIsoToDate( + billing.createdAt ?? '') + .toPersianDate(), + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface))), + DataCell(Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: billing.status == 'success' + ? AppColors.green.defaultShade + : billing.status == 'failed' + ? AppColors.red.defaultShade + : Colors.orangeAccent, + borderRadius: BorderRadius.circular(4)), + child: Text( + billing.status == 'success' + ? 'موفق' + : billing.status == 'failed' + ? 'ناموفق' + : 'لغو شده', + style: AppTextStyles.body4.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold), + ), + )), + DataCell(Text( + billing.refId != null && billing.refId!.isNotEmpty + ? billing.refId! + : '-', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface))), + ]); + }, + ), + ); + + return ValueListenableBuilder( + valueListenable: index, + builder: (context, selectedIndex, _) { + return Directionality( + textDirection: TextDirection.rtl, + child: Column( + children: [ + SingleChildScrollView( + controller: scrollController, + scrollDirection: Axis.horizontal, + child: Container( + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: + Colors.black.withValues(alpha: 0.1), + offset: const Offset(0, 0), + blurRadius: 12, + spreadRadius: 0, + ) + ], + borderRadius: BorderRadius.circular(16), + color: + Theme.of(context).colorScheme.surface), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: DataTable( + columns: dataColumns, + rows: dataRows[selectedIndex]), + )), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Opacity( + opacity: index.value > 0 ? 1 : 0, + child: CircleIconBtn( + color: + context.read().isDark() + ? AppColors.black[900] + : AppColors.primaryColor[50], + iconColor: + Theme.of(context).colorScheme.primary, + onTap: () { + if (index.value > 0) { + index.value--; + } + }, + icon: Assets.icon.outline.arrowRight), + ), + const SizedBox( + width: 16, + ), + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + '${selectedIndex + 1}', + style: AppTextStyles.body3.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface, + fontWeight: FontWeight.bold), + ), + ), + const SizedBox( + width: 16, + ), + Opacity( + opacity: + index.value < dataRows.length - 1 ? 1 : 0, + child: Transform.rotate( + angle: pi, + child: CircleIconBtn( + color: context + .read() + .isDark() + ? AppColors.black[900] + : AppColors.primaryColor[50], + iconColor: Theme.of(context) + .colorScheme + .primary, + onTap: () { + if (index.value < + dataRows.length - 1) { + index.value++; + } + }, + icon: Assets.icon.outline.arrowRight)), + ), + ], + ) + ], + ), + ); + }); + } + return DefaultPlaceHolder( + enabled: (state is PaymantHistoryLoading || + state is PaymantHistoryInitial), + child: Container( + width: MediaQuery.sizeOf(context).width, + height: state is PaymantHistoryEmpty ? null : 200, + margin: EdgeInsets.symmetric( + horizontal: Responsive(context).isMobile() ? 16 : 90.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Theme.of(context).colorScheme.surface), + alignment: Alignment.center, + child: state is PaymantHistoryEmpty + ? EmptyStates.getEmptyState( + scale: 0.8, + status: EmptyStatesEnum.amount, + title: 'تاریخچه‌ای وجود ندارد') + : null, + )); + }, + ), + ], + ); + } + + Padding infoes() { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + BlocBuilder(builder: (context, state) { + return Expanded( + child: cardInfo( + icon: Assets.icon.outline.coin, + title: 'اعتبار باقی مانده', + value: + '${((UserInfoCubit.userInfoModel.credit ?? 0) + (UserInfoCubit.userInfoModel.freeCredit ?? 0) + (UserInfoCubit.userInfoModel.gift_credit ?? 0))} سکه', + btnTitle: 'برای افزایش اعتبار به باسا مراجعه کنید', + onClick: () async { + // context.go(Routes.purchase); + }, + ), + ); + }), + // const SizedBox( + // width: 16, + // ), + // BlocBuilder( + // builder: (context, state) { + // return Expanded( + // child: cardInfo( + // icon: Assets.icon.outline.emptyWalletTick, + // title: 'درآمد', + // value: '${UserInfoCubit.userInfoModel.income ?? 0} سکه', + // btnTitle: 'تسویه حساب', + // onClick: () { + // DialogHandler(context: context).showCoin( + // onSuccess: () { + // final user = UserInfoCubit.userInfoModel; + // user.income = 0; + // context.read().changeUser(user); + // }, + // ); + // }, + // ), + // ); + // }, + // ) + ], + ), + ); + } + + Container cardInfo( + {required final SvgGenImage icon, + required final String title, + required final String value, + required final String btnTitle, + final Function()? onClick}) { + return Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + const SizedBox( + height: 16, + ), + CircleIconBtn( + size: 48, + iconPadding: const EdgeInsets.all(8), + icon: icon, + color: AppColors.secondryColor[50], + iconColor: AppColors.secondryColor.defaultShade, + ), + const SizedBox( + height: 12, + ), + Text( + title, + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface), + ), + const SizedBox( + height: 4, + ), + Text( + value, + style: AppTextStyles.body4.copyWith( + color: AppColors + .gray[context.read().isDark() ? 600 : 900]), + textDirection: TextDirection.rtl, + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: LoadingButton( + width: double.infinity, + color: context.read().isDark() + ? AppColors.black[900] + : AppColors.primaryColor[50], + onPressed: onClick, + radius: 8, + child: Text( + btnTitle, + textAlign: TextAlign.center, + style: AppTextStyles.body6 + .copyWith(color: Theme.of(context).colorScheme.primary), + ), + ), + ), + const SizedBox( + height: 12, + ), + ], + ), + ); + } +} diff --git a/lib/ui/screens/setting/setting_page.dart b/lib/ui/screens/setting/setting_page.dart new file mode 100644 index 0000000..c2dceb5 --- /dev/null +++ b/lib/ui/screens/setting/setting_page.dart @@ -0,0 +1,1216 @@ +// ignore_for_file: deprecated_member_use_from_same_package, use_build_context_synchronously + +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/ui/screens/setting/cubit/ad_remaining_cubit.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/animations/animated_visibility.dart'; +import 'package:hoshan/ui/widgets/components/dialog/dialog_handler.dart'; +import 'package:hoshan/ui/widgets/components/dropdown/more_popup_menu.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:persian_number_utility/persian_number_utility.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class SettingPage extends StatefulWidget { + const SettingPage({super.key}); + + @override + State createState() => _SettingPageState(); +} + +class _SettingPageState extends State { + final ValueNotifier isDark = ValueNotifier(false); + final ValueNotifier showCredit = ValueNotifier(false); + final GlobalKey containerKey = GlobalKey(); + + Timer? timer; + late final ValueNotifier countdown = ValueNotifier(3600); + late final ValueNotifier maxSeconds = ValueNotifier(3600); + late final ValueNotifier coinsForAd = ValueNotifier(0); + late final ValueNotifier enableAd = ValueNotifier(true); + late final ValueNotifier loadingAdShow = ValueNotifier(false); + + void startCountdown() { + timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (countdown.value > 0) { + countdown.value -= 1; + } else { + timer.cancel(); + } + }); + } + + @override + void dispose() { + timer?.cancel(); + countdown.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + // appBar: ReversibleAppbar( + // context, + // title: Row( + // mainAxisAlignment: MainAxisAlignment.end, + // children: [ + // Text( + // 'تنظیمات', + // style: AppTextStyles.body3 + // .copyWith(color: Theme.of(context).colorScheme.onSurface), + // ), + // const SizedBox( + // width: 8, + // ), + // Assets.icon.outline.setting.svg( + // width: Responsive(context).isMobile() ? null : 32, + // color: Theme.of(context).colorScheme.onSurface), + // ], + // ), + // ), + // persistentFooterAlignment: AlignmentDirectional.center, + body: Responsive(context).maxWidthInDesktop( + child: (contxet, maxWidth) => RefreshIndicator( + onRefresh: () async { + context.read().getRemainingAd(); + }, + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + BlocBuilder( + builder: (context, state) { + return Container( + margin: const EdgeInsets.fromLTRB(16, 32, 16, 0), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(18), + border: + Border.all(color: AppColors.gray.defaultShade)), + child: Directionality( + textDirection: TextDirection.rtl, + child: Row( + children: [ + Container( + width: 78, + height: 78, + decoration: BoxDecoration( + color: context.read().state == + ThemeMode.dark + ? Theme.of(context).colorScheme.onSurface + : AppColors.gray[300], + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + blurRadius: 6, + color: const Color(0xff4d4d4d) + .withValues(alpha: 0.4)) + ]), + child: ClipOval( + child: UserInfoCubit.userInfoModel.image != null + ? ImageNetwork( + url: DioService.baseURL + + UserInfoCubit.userInfoModel.image!, + showHero: true, + ) + : Padding( + padding: const EdgeInsets.all(12), + child: + Assets.icon.outline.profile.svg(), + ), + ), + ), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (UserInfoCubit.userInfoModel.username != + null) + Text( + UserInfoCubit.userInfoModel.username!, + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + if (UserInfoCubit.userInfoModel.mobileNumber != + null) + Text( + UserInfoCubit.userInfoModel.mobileNumber!, + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + const SizedBox( + height: 8, + ), + GestureDetector( + onTap: () { + context.go(Routes.editProfile); + }, + child: Row( + children: [ + Icon( + Icons.edit_outlined, + color: + AppColors.primaryColor.defaultShade, + size: 18, + ), + Text( + 'ویرایش', + style: AppTextStyles.body5.copyWith( + color: Theme.of(context) + .colorScheme + .primary, + fontWeight: FontWeight.bold), + ), + ], + ), + ) + ], + ), + ], + ), + ), + ); + }, + ), + const SizedBox( + height: 12, + ), + // BlocConsumer( + // listener: (context, state) { + // if (state is AdRemainingSuccess) { + // maxSeconds.value = + // int.tryParse(state.data['ads_cooldown'] ?? '0') ?? 0; + // countdown.value = int.tryParse( + // state.data['remaining_seconds'] ?? '0') ?? + // 0; + // coinsForAd.value = + // int.tryParse(state.data['coins_per_video'] ?? '0') ?? + // 0; + // enableAd.value = state.data['ads_enabled'] == 'true'; + // startCountdown(); + // } + // }, + // builder: (context, state) { + // if (state is AdRemainingFail) { + // return const SizedBox.shrink(); + // } + // return DefaultPlaceHolder( + // enabled: state is AdRemainingLoading, + // child: Container( + // margin: const EdgeInsets.fromLTRB(16, 0, 16, 0), + // padding: const EdgeInsets.all(16), + // decoration: BoxDecoration( + // color: Theme.of(context).colorScheme.surface, + // borderRadius: BorderRadius.circular(18), + // border: + // Border.all(color: AppColors.gray.defaultShade)), + // child: Directionality( + // textDirection: TextDirection.rtl, + // child: ValueListenableBuilder( + // valueListenable: enableAd, + // builder: (context, enabled, _) { + // return Column( + // children: [ + // Row( + // children: [ + // Container( + // width: 78, + // height: 78, + // decoration: BoxDecoration( + // color: context + // .read< + // ThemeModeCubit>() + // .state == + // ThemeMode.dark + // ? Theme.of(context) + // .colorScheme + // .onSurface + // : AppColors.gray[300], + // shape: BoxShape.circle, + // boxShadow: [ + // BoxShadow( + // blurRadius: 6, + // color: + // const Color(0xff4d4d4d) + // .withValues( + // alpha: 0.4)) + // ]), + // child: ClipOval( + // child: Padding( + // padding: const EdgeInsets.all(12), + // child: Assets.icon.bulk.coin + // .image( + // color: enabled + // ? null + // : AppColors.gray + // .defaultShade, + // colorBlendMode: + // BlendMode.color), + // ), + // ), + // ), + // const SizedBox( + // width: 12, + // ), + // Expanded( + // child: Column( + // crossAxisAlignment: + // CrossAxisAlignment.start, + // children: [ + // Text( + // 'شکار سکه‌ها', + // style: AppTextStyles.body3 + // .copyWith( + // fontWeight: + // FontWeight.bold, + // color: enabled + // ? Theme.of(context) + // .colorScheme + // .primary + // : AppColors.gray + // .defaultShade), + // ), + // Text( + // 'کلیک کن و به ازای تماشای ویدیو، سکه رایگان بگیر', + // style: AppTextStyles.body4 + // .copyWith( + // color: enabled + // ? Theme.of(context) + // .colorScheme + // .primary + // : AppColors.gray + // .defaultShade), + // ), + // ], + // ), + // ), + // ], + // ), + // const SizedBox( + // height: 8, + // ), + // Divider(), + // const SizedBox( + // height: 8, + // ), + // enabled + // ? Row( + // crossAxisAlignment: + // CrossAxisAlignment.start, + // children: [ + // Column( + // children: [ + // CircleIconBtn( + // icon: Assets + // .icon.outline.coin, + // iconColor: AppColors + // .secondryColor + // .defaultShade, + // color: AppColors + // .secondryColor[50], + // ), + // SizedBox( + // height: 4, + // ), + // ValueListenableBuilder( + // valueListenable: + // coinsForAd, + // builder: + // (context, coins, _) { + // return Text( + // '$coins سکه هدیه', + // style: AppTextStyles + // .body5 + // .copyWith( + // color: Theme.of( + // context) + // .colorScheme + // .onSurface), + // ); + // }) + // ], + // ), + // SizedBox( + // width: 12, + // ), + // Expanded( + // flex: 2, + // child: + // ValueListenableBuilder( + // valueListenable: + // countdown, + // builder: (context, + // value, child) { + // if (value == 0) { + // return ValueListenableBuilder( + // valueListenable: + // loadingAdShow, + // builder: + // (context, + // loading, + // _) { + // return LoadingButton( + // loading: + // loading, + // onPressed: + // () async { + // loadingAdShow.value = + // true; + // await TapsellService.showAdRewarded( + // context); + // loadingAdShow.value = + // false; + // }, + // child: + // Center( + // child: + // Text( + // 'نمایش تبلیغ', + // style: AppTextStyles.body3.copyWith( + // color: Colors.white, + // fontWeight: FontWeight.bold), + // ), + // )); + // }); + // } + // return Column( + // crossAxisAlignment: + // CrossAxisAlignment + // .end, + // children: [ + // SizedBox( + // height: 32, + // child: Center( + // child: + // Directionality( + // textDirection: + // TextDirection + // .ltr, + // child: ValueListenableBuilder( + // valueListenable: maxSeconds, + // builder: (context, max, _) { + // return LinearProgressIndicator( + // borderRadius: + // BorderRadius.circular(16), + // color: + // AppColors.primaryColor.defaultShade, + // backgroundColor: + // AppColors.gray[200], + // value: + // ((max - value) / max), // Reverse the progress value + // ); + // }), + // ), + // ), + // ), + // SizedBox( + // height: 4, + // ), + // Text( + // '${DateTimeUtils.getFormattedTimeFromSeconds(value)} زمان باقی‌مانده تا تبلیغ بعدی', + // style: AppTextStyles + // .body5 + // .copyWith( + // color: Theme.of(context) + // .colorScheme + // .onSurface), + // ) + // ], + // ); + // }), + // ) + // ], + // ) + // : Text( + // 'تا اطلاع ثانوی این سرویس در دسترس نیست.', + // style: AppTextStyles.body5.copyWith( + // color: + // AppColors.red.defaultShade), + // ) + // ], + // ); + // }), + // ), + // ), + // ); + // }, + // ), + + // const SizedBox( + // height: 4, + // ), + // BlocBuilder( + // builder: (context, state) { + // return UserInfoCubit.userInfoModel.cardNumber == null + // ? const SizedBox.shrink() + // : cardContainer(); + // }, + // ), + + // const SizedBox( + // height: 4, + // ), + + // GestureDetector( + // onTap: () { + // context.go(Routes.income); + // }, + // child: Padding( + // padding: + // const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + // child: CustomeBanner( + // BannerModel( + // child: Padding( + // padding: const EdgeInsets.only(left: 32.0), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.start, + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // Text( + // 'فعال سازی درآمد \nاز طریق ساخت دستیار', + // textDirection: TextDirection.rtl, + // textAlign: TextAlign.center, + // style: AppTextStyles.body4.copyWith( + // fontWeight: FontWeight.bold, + // color: Colors.white), + // ), + // ], + // ), + // ), + // imageUrl: '/banner/setting.jpeg', + // colors: [ + // AppColors.secondryColor[200].withAlpha(50), + // AppColors.secondryColor[200].withAlpha(80), + // AppColors.secondryColor[300], + // AppColors.secondryColor[400], + // AppColors.secondryColor.defaultShade + // ]), + // width: double.infinity, + // height: Responsive(context).isMobile() ? 120 : 180), + // ), + // ), + + SizedBox( + height: 4, + ), + // settingContainer( + // title: 'افزایش اعتبار', + // icon: Assets.icon.outline.crown, + // onPressed: () { + // context.go(Routes.purchase); + // }, + // ), + if (UserInfoCubit.userInfoModel.code != null) + // settingContainer( + // title: 'کد معرف', + // icon: Assets.icon.outline.profileUserDoual, + // widget: Text(UserInfoCubit.userInfoModel.code!, + // style: AppTextStyles.body4.copyWith( + // color: Theme.of(context).colorScheme.onSurface)), + // onPressed: () async { + // await Clipboard.setData(ClipboardData( + // text: UserInfoCubit.userInfoModel.code!)); + // Future.delayed( + // Duration.zero, + // () => SnackBarManager(context, id: 'Copy').show( + // status: SnackBarStatus.info, + // message: 'متن کپی شد 😃')); + // }, + // ), + settingContainer( + title: 'گزارش میزان مصرف', + icon: Assets.icon.outline.chart, + onPressed: () { + context.go(Routes.utilizationReport); + }, + ), + settingContainer( + title: 'حساب من', + icon: Assets.icon.outline.cardPos, + onPressed: () { + context.go(Routes.myAccount); + }, + ), + GestureDetector( + onTap: () { + final themeState = context.read().state; + MorePopupMenuHandler(context: context).showMorePopupMenu( + containerKey: containerKey, + color: themeState == ThemeMode.dark + ? AppColors.black[500] + : Colors.white, + items: [ + PopUpMenuItemModel( + popupMenuItem: PopupMenuItem( + value: 0, + child: MorePopupMenuHandler.morePopUpItem( + icon: Assets.icon.outline.moon, + title: 'تیره', + color: + Theme.of(context).colorScheme.onSurface), + ), + click: () async { + context.read().setDarkMode(); + }, + ), + PopUpMenuItemModel( + popupMenuItem: PopupMenuItem( + value: 1, + child: MorePopupMenuHandler.morePopUpItem( + icon: Assets.icon.outline.sun, + title: 'روشن', + color: + Theme.of(context).colorScheme.onSurface), + ), + click: () async { + context.read().setLightkMode(); + }, + ), + PopUpMenuItemModel( + popupMenuItem: PopupMenuItem( + value: 2, + child: MorePopupMenuHandler.morePopUpItem( + icon: Assets.icon.outline.setting, + title: 'سیستم', + color: + Theme.of(context).colorScheme.onSurface), + ), + click: () async { + context.read().setDefaultSystem(); + }, + ), + ]); + }, + child: Container( + key: containerKey, + child: settingContainer( + title: 'حالت نمایش', + icon: Assets.icon.outline.brush, + widget: BlocBuilder( + builder: (context, state) { + final mode = context + .read() + .whatIsThemeMode(); + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + (mode == 'dark' + ? Assets.icon.outline.moon + : mode == 'light' + ? Assets.icon.outline.sun + : Assets.icon.outline.setting) + .svg( + color: Theme.of(context) + .colorScheme + .onSurface), + const SizedBox( + width: 8, + ), + Text( + mode == 'dark' + ? 'تیره' + : mode == 'light' + ? 'روشن' + : 'سیستم', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + ], + ); + }, + )), + ), + ), + + // Stack( + // children: [ + // settingContainer( + // title: 'دریافت اعلانات', + // icon: Assets.icon.outline.notificationBing, + // widget: const SizedBox()), + // Positioned( + // left: 32, + // top: 16, + // bottom: 16, + // child: LiteRollingSwitch( + // value: false, + // textOn: '', + // textOff: '', + // colorOn: AppColors.primaryColor.defaultShade, + // colorOff: AppColors.gray.defaultShade, + // onChanged: (bool state) { + // //Use it to manage the different states + // isDark.value = !state; + // }, + // )) + // ], + // ), + + settingContainer( + title: 'قوانین و حریم خصوصی', + icon: Assets.icon.outline.shieldTick, + onPressed: () async { + await launchUrl( + Uri.parse( + 'https://houshan.ai/%D9%82%D9%88%D8%A7%D9%86%DB%8C%D9%86-%D9%88-%D9%85%D9%82%D8%B1%D8%A7%D8%B1%D8%A7%D8%AA-%D8%AD%D9%81%D8%B8-%D8%AD%D8%B1%DB%8C%D9%85-%D8%AE%D8%B5%D9%88%D8%B5%DB%8C-%DA%A9%D8%A7%D8%B1%D8%A8%D8%B1%D8%A7%D9%86/'), + mode: LaunchMode.externalApplication) + .onError( + (error, stackTrace) { + if (kDebugMode) { + print('error open Link is: $error'); + } + return false; + }, + ); + }), + + // animatedSettingContainer( + // title: 'درباره ما', + // icon: Assets.icon.outline.more, + // childrens: [ + // const SizedBox( + // height: 24, + // ), + // settingContainer( + // title: 'سایر محصولات', + // icon: Assets.icon.outline.mobile, + // notBorder: true, + // notMargin: true, + // notPadding: true, + // onPressed: () { + // context.go(Routes.otherProducts); + // }, + // ), + // const SizedBox( + // height: 8, + // ), + // Divider( + // color: AppColors.gray.defaultShade, + // ), + // const SizedBox( + // height: 12, + // ), + // settingContainer( + // title: 'تازه‌ترین‌های هوشان', + // icon: Assets.icon.outline.lampCharge, + // notBorder: true, + // notMargin: true, + // onPressed: () async { + // await launchUrl(Uri.parse('https://Houshan.ai'), + // mode: LaunchMode.externalApplication) + // .onError( + // (error, stackTrace) { + // if (kDebugMode) { + // print('error open Link is: $error'); + // } + // return false; + // }, + // ); + // }, + // notPadding: true), + // ]), + + settingContainer( + title: 'ارسال تیکت به پشتیبانی', + icon: Assets.icon.outline.messages, + onPressed: () { + context.go(Routes.ticket); + }, + ), + + // settingContainer( + // title: 'نمایش تبلیغ', + // icon: Assets.icon.outline.messages, + // onPressed: () async { + // await showDialog( + // context: context, + // builder: (context) => Dialog( + // insetPadding: EdgeInsets.all(16), + // child: Column( + // mainAxisSize: MainAxisSize.min, + // children: [ + // SizedBox( + // height: 16, + // ), + // ElevatedButton( + // onPressed: () async { + // await TapsellService.showAdInterstitial(); + // }, + // child: Text('INTERSTITIAL')), + // SizedBox( + // height: 16, + // ), + // ElevatedButton( + // onPressed: () async { + // await TapsellService.showAdRewarded(context); + // }, + // child: Text('REWARDED')), + // SizedBox( + // height: 16, + // ), + // ElevatedButton( + // onPressed: () async { + // await TapsellService.showAdBanner( + // context); + // }, + // child: Text('AD')), + // SizedBox( + // height: 16, + // ), + // ], + // ), + // )); + // }, + // ), + + settingContainer( + title: 'سوالات متداول', + icon: Assets.icon.outline.messageQuestion, + onPressed: () => context.go(Routes.faq)), + const SizedBox( + height: 16, + ), + GestureDetector( + onTap: () async { + try { + await Share.share('هوشان؛ دوست هوش مصنوعی تو\n' + 'هر سوالی داری، هر تصمیمی که می‌خوای بگیری یا هر ایده‌ی خلاقانه‌ای که داری و می‌خوای به واقعیت تبدیلش کنی، هوشان همیشه هست تا کمکت کنه!\n' + 'کد معرف: ${UserInfoCubit.userInfoModel.code}\n' + 'https://houshan.ai/'); + } catch (e) { + if (kDebugMode) { + print('Error in share Text: $e'); + } + } + }, + child: Padding( + padding: const EdgeInsets.fromLTRB(32, 16, 32, 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + 'اشتراک گذاری', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.primary), + ), + const SizedBox( + width: 8, + ), + Assets.icon.outline.share + .svg(color: Theme.of(context).colorScheme.primary), + ], + ), + ), + ), + GestureDetector( + onTap: () { + DialogHandler(context: context).showLogOut(); + }, + child: Padding( + padding: const EdgeInsets.fromLTRB(32, 16, 32, 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + 'خروج از حساب کاربری', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.secondary), + ), + const SizedBox( + width: 8, + ), + Assets.icon.outline.login.svg( + color: Theme.of(context).colorScheme.secondary), + ], + ), + ), + ), + const SizedBox( + height: 16, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // InkWell( + // onTap: () async { + // await launchUrl( + // Uri.parse('https://t.me/houshanAiBot'), + // mode: LaunchMode.externalApplication) + // .onError( + // (error, stackTrace) { + // if (kDebugMode) { + // print('error open Link is: $error'); + // } + // return false; + // }, + // ); + // }, + // child: Assets.icon.social.bold.telegram.svg( + // width: 32, + // height: 32, + // color: Theme.of(context).colorScheme.primary), + // ), + const SizedBox( + width: 16, + ), + InkWell( + onTap: () async { + await launchUrl( + Uri.parse( + 'https://www.instagram.com/Houshan.ai'), + mode: LaunchMode.externalApplication) + .onError( + (error, stackTrace) { + if (kDebugMode) { + print('error open Link is: $error'); + } + return false; + }, + ); + }, + child: Assets.icon.social.bold.instagram.svg( + width: 32, + height: 32, + color: Theme.of(context).colorScheme.primary), + ), + const SizedBox( + width: 16, + ), + InkWell( + onTap: () async { + await launchUrl(Uri.parse('https://houshan.ai/'), + mode: LaunchMode.externalApplication) + .onError( + (error, stackTrace) { + if (kDebugMode) { + print('error open Link is: $error'); + } + return false; + }, + ); + }, + child: Assets.icon.social.bold.site.svg( + width: 32, + height: 32, + color: Theme.of(context).colorScheme.primary), + ), + ], + ), + ), + const SizedBox( + height: 16, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: (context, snapshot) { + String version = '...'; + if (snapshot.hasData && snapshot.data != null) { + version = snapshot.data!.version; + } + return Text( + 'ورژن: $version', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ); + }) + ], + ), + const SizedBox( + height: 16, + ), + ], + ), + ), + ), + ), + ); + } + + Widget cardContainer() { + return BlocBuilder( + builder: (context, state) { + return ValueListenableBuilder( + valueListenable: showCredit, + builder: (context, show, child) { + return Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16)), + border: Border.all( + color: Theme.of(context).colorScheme.primary)), + child: Column( + children: [ + const SizedBox( + height: 12, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + show + ? UserInfoCubit.userInfoModel.cardNumber! + .charRagham(separator: ' - ') + : '**** - **** - **** - ****', + style: AppTextStyles.body3.copyWith( + color: + Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold), + ), + Text( + show + ? UserInfoCubit.userInfoModel.cardNumber! + .replaceAll(' ', '') + .getBankNameFromCardNumber() + : '**** **** **', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ) + ], + ), + const SizedBox( + height: 12, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text('سکه', + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context) + .colorScheme + .primary)), + const SizedBox( + width: 4, + ), + Text( + show + ? UserInfoCubit.userInfoModel.income + .toString() + .seRagham() + : '*****', + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context) + .colorScheme + .secondary)), + ], + ), + GestureDetector( + onTap: () => showCredit.value = !show, + child: Row( + children: [ + Text('موجودی', + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context) + .colorScheme + .primary)), + const SizedBox( + width: 8, + ), + Padding( + padding: + const EdgeInsets.only(bottom: 4.0), + child: Icon( + show + ? CupertinoIcons.eye + : CupertinoIcons.eye_slash, + size: 20, + color: Theme.of(context) + .colorScheme + .secondary, + ), + ), + ], + ), + ), + ], + ) + ], + ), + ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + border: Border.all( + color: Theme.of(context).colorScheme.primary), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(16), + bottomRight: Radius.circular(16))), + child: Center( + child: GestureDetector( + onTap: () => DialogHandler(context: context).showCoin( + onSuccess: () { + final user = UserInfoCubit.userInfoModel; + user.income = 0; + context.read().changeUser(user); + }, + ), + child: Text( + 'تسویه حساب', + style: AppTextStyles.headline6 + .copyWith(color: Colors.white), + ), + ), + ), + ) + ], + ), + ); + }); + }, + ); + } + + Widget settingContainer({ + required final String title, + required final SvgGenImage icon, + final Widget? widget, + final Function()? onPressed, + final bool notBorder = false, + final bool notMargin = false, + final bool notPadding = false, + }) { + return GestureDetector( + onTap: onPressed, + child: Container( + margin: notMargin + ? EdgeInsets.zero + : const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + padding: notPadding + ? EdgeInsets.zero + : const EdgeInsets.symmetric(horizontal: 16, vertical: 18), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: notBorder + ? null + : Border.all(color: AppColors.gray.defaultShade), + borderRadius: BorderRadius.circular(18)), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + widget ?? + Icon( + Icons.arrow_back_ios_new_rounded, + size: 18, + color: AppColors.secondryColor.defaultShade, + ), + Row( + children: [ + Text( + title, + style: AppTextStyles.body4 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ), + const SizedBox( + width: 8, + ), + icon.svg(color: Theme.of(context).colorScheme.primary), + ], + ), + ], + ), + ), + ); + } + + Widget animatedSettingContainer( + {required final String title, + required final SvgGenImage icon, + required final List childrens}) { + final ValueNotifier show = ValueNotifier(false); + return GestureDetector( + onTap: () { + show.value = !show.value; + }, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border.all(color: AppColors.gray.defaultShade), + borderRadius: BorderRadius.circular(18)), + child: ValueListenableBuilder( + valueListenable: show, + builder: (context, isVisible, child) => Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Transform.rotate( + angle: (isVisible ? 90 : -90) * pi / 180, + child: Icon( + Icons.arrow_back_ios_new_rounded, + size: 18, + color: AppColors.secondryColor.defaultShade, + ), + ), + Row( + children: [ + Text( + title, + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + const SizedBox( + width: 8, + ), + icon.svg(color: AppColors.primaryColor.defaultShade), + ], + ), + ], + ), + AnimatedVisibility( + isVisible: isVisible, + duration: const Duration(milliseconds: 300), + child: Column( + children: childrens, + )) + ], + ), + ), + ), + ); + } +} diff --git a/lib/ui/screens/setting/utilization_report_page.dart b/lib/ui/screens/setting/utilization_report_page.dart new file mode 100644 index 0000000..84c8c97 --- /dev/null +++ b/lib/ui/screens/setting/utilization_report_page.dart @@ -0,0 +1,419 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:hoshan/core/utils/date_time.dart'; +import 'package:hoshan/data/model/empty_states_enum.dart'; +import 'package:hoshan/ui/screens/setting/bloc/report_of_use_bloc.dart'; +import 'package:hoshan/ui/screens/setting/cubit/report_pi_coin_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/chart/custome_pi_chart.dart'; +import 'package:hoshan/ui/widgets/components/dialog/dialog_handler.dart'; +import 'package:hoshan/ui/widgets/components/text/credit_cost.dart'; +import 'package:hoshan/ui/widgets/sections/empty/empty_states.dart'; +import 'package:hoshan/ui/widgets/sections/header/reversible_appbar.dart'; +import 'package:shamsi_date/shamsi_date.dart'; + +class UtilizationReportPage extends StatefulWidget { + const UtilizationReportPage({super.key}); + + @override + State createState() => _UtilizationReportPageState(); +} + +class _UtilizationReportPageState extends State { + ValueNotifier selectedDate = + ValueNotifier(DateTimeUtils.getNowJalali()); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: ReversibleAppbar( + context, + titleText: 'گزارش میزان مصرف', + ), + body: Responsive(context).builder( + desktop: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox( + height: 16, + ), + calenderBtn(), + const SizedBox( + height: 16, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: chartBar(), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: piChart(context), + ), + ], + ), + ], + ), + ), + ), + mobile: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + children: [ + const SizedBox( + height: 16, + ), + calenderBtn(), + const SizedBox( + height: 16, + ), + chartBar(), + piChart(context) + ], + ), + ), + ), + ); + } + + ValueListenableBuilder calenderBtn() { + return ValueListenableBuilder( + valueListenable: selectedDate, + builder: (context, date, _) { + return LoadingButton( + onPressed: () { + DialogHandler(context: context).showShamsiYearMonthPicker( + initailDate: date, + onDateSelected: (selectedDate) { + this.selectedDate.value = selectedDate; + + context.read().add(GetReport( + startDate: selectedDate, + )); + context.read().getReport( + startDate: selectedDate, + ); + }, + ); + }, + child: Text( + '${date.formatter.mN} ${date.year}', + style: AppTextStyles.body4.copyWith(color: Colors.white), + )); + }); + } + + Container piChart(BuildContext context) { + return Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(10)), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'اعتبار باقی مانده', + style: AppTextStyles.headline6 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'سکه', + style: AppTextStyles.body4 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ), + const SizedBox( + width: 8, + ), + const CreditCost(), + ], + ), + BlocBuilder( + builder: (context, state) { + if (state is ReportPiCoinEmpty || state is ReportPiCoinFail) { + return Container( + width: Responsive(context).isDesktop() + ? 400 + : MediaQuery.sizeOf(context).width * 0.7, + height: Responsive(context).isDesktop() + ? 400 + : MediaQuery.sizeOf(context).width * 0.7, + margin: const EdgeInsets.symmetric(horizontal: 32), + padding: const EdgeInsets.all(38), + decoration: BoxDecoration( + border: Border.all( + color: AppColors.secondryColor.defaultShade, + width: 38), + shape: BoxShape.circle), + child: Center( + child: Text( + 'داده ای موجود نیست', + style: AppTextStyles.headline5.copyWith( + color: Theme.of(context).colorScheme.onSurface), + textAlign: TextAlign.center, + ), + ), + ); + } + if (state is ReportPiCoinSuccess) { + return CustomePiChart( + points: state.points, + ); + } + return Padding( + padding: const EdgeInsets.all(32.0), + child: SpinKitDualRing( + color: AppColors.secondryColor.defaultShade, + size: Responsive(context).isDesktop() + ? 200 + : MediaQuery.sizeOf(context).width / 3, + lineWidth: Responsive(context).isDesktop() + ? 50 + : MediaQuery.sizeOf(context).width / 8, + ), + ); + }, + ), + ], + ), + ); + } + + BlocBuilder chartBar() { + return BlocBuilder( + builder: (context, state) { + if (state is ReportOfUseSuccess) { + try { + int maxVal = state.reportModel.report!.fold( + 100, + (previousValue, report) { + try { + if (report.coinUsage! > previousValue) { + return report.coinUsage!; + } + } catch (e) { + if (kDebugMode) { + print('Error is: $e'); + } + } + + return previousValue; + }, + ); + maxVal = maxVal + (maxVal % 100 == 0 ? 0 : (100 - (maxVal % 100))); + final List leftTitle = [0]; + do { + leftTitle.add((leftTitle.last + (maxVal / 5).round())); + } while (leftTitle.length < 6); + final points = state.reportModel.report!.map( + (e) { + final date = DateTimeUtils.getDateFromString(false, e.date!); + return FlSpot( + date.day.toDouble(), ((e.coinUsage! * 5) / maxVal)); + }, + ).toList(); + + return AspectRatio( + aspectRatio: 4 / 3, + child: Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(10)), + child: LineChart(LineChartData( + borderData: FlBorderData(show: false), + gridData: FlGridData( + show: true, + horizontalInterval: 1, + verticalInterval: 5, + getDrawingHorizontalLine: (value) => FlLine( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900] + .withAlpha(50), + strokeWidth: 0.5), + getDrawingVerticalLine: (value) => FlLine( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900] + .withAlpha(50), + strokeWidth: 0.5), + ), + titlesData: FlTitlesData( + show: true, + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false)), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false)), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + interval: 5, + getTitlesWidget: (value, meta) { + return SideTitleWidget( + fitInside: + SideTitleFitInsideData.fromTitleMeta(meta), + axisSide: AxisSide.bottom, + child: Center( + child: Padding( + padding: EdgeInsets.only( + left: value == 30 ? 40 : 0, + right: value == 1 ? 12 : 0), + child: Text( + value == 31 ? '' : '${value.round()}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTextStyles.body6.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + ), + ), + ); + }, + )), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 36, + interval: 1, + getTitlesWidget: (value, meta) { + return Padding( + padding: EdgeInsets.only( + top: value == 0 ? 12 : 0.0, right: 8), + child: Text( + value == 0 + ? 'سکه\nروز' + : '${leftTitle[value.round()]}', + textAlign: TextAlign.start, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: AppTextStyles.body6.copyWith( + color: + Theme.of(context).colorScheme.onSurface), + ), + ); + }, + ))), + minX: 1, + maxX: 31, + minY: 0, + maxY: 5, + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + getTooltipColor: (touchedSpot) => + Theme.of(context).colorScheme.onSurface, + getTooltipItems: (List touchedBarSpots) { + return touchedBarSpots.map((barSpot) { + final flSpot = barSpot; + if (flSpot.x == 0 || flSpot.x == 6) { + return null; + } + + final date = DateTimeUtils.getDateFromString( + false, + state.reportModel.report![flSpot.spotIndex] + .date!) + .formatter; + + return LineTooltipItem( + '${(date.wN).replaceAll(' ', '\u200C')} ${date.d} ${date.mN} \n${state.reportModel.report![flSpot.spotIndex].coinUsage ?? 0} سکه', + AppTextStyles.body5.copyWith( + color: Theme.of(context).colorScheme.surface, + ), + textDirection: TextDirection.rtl); + }).toList(); + }, + ), + ), + lineBarsData: [ + LineChartBarData( + spots: points, + isCurved: false, + curveSmoothness: 0.2, + gradient: LinearGradient(colors: [ + Theme.of(context).colorScheme.primary, + Theme.of(context).colorScheme.secondary, + ]), + barWidth: 1.5, + isStrokeCapRound: true, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.primary, + Theme.of(context).colorScheme.secondary, + ] + .map( + (color) => color.withValues(alpha: 0.3)) + .toList()))) + ], + backgroundColor: Theme.of(context).colorScheme.surface, + )), + ), + ); + } catch (e) { + if (kDebugMode) { + print('Error is: $e'); + } + return const SizedBox.shrink(); + } + } + return state is ReportOfUseFail || state is ReportOfUseEmpty + ? Container( + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(10)), + child: Center( + child: EmptyStates.getEmptyState( + scale: 0.8, + status: EmptyStatesEnum.amount, + title: 'داده‌ای برای نمایش وجود ندارد'), + ), + ) + : AspectRatio( + aspectRatio: 4 / 3, + child: Container( + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(10)), + child: Center( + child: SpinKitThreeBounce( + color: Theme.of(context).colorScheme.primary, + size: 46, + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/ui/screens/splash/cubit/user_info_cubit.dart b/lib/ui/screens/splash/cubit/user_info_cubit.dart new file mode 100644 index 0000000..dbf9b22 --- /dev/null +++ b/lib/ui/screens/splash/cubit/user_info_cubit.dart @@ -0,0 +1,61 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/data/model/ai/credit_model.dart'; +import 'package:hoshan/data/model/auth/user_info_model.dart'; +import 'package:hoshan/data/repository/auth_repository.dart'; + +part 'user_info_state.dart'; + +class UserInfoCubit extends Cubit { + UserInfoCubit() : super(UserInfoInitial()); + static UserInfoModel userInfoModel = UserInfoModel(); + + Future getUserInfo() async { + if (state is UserInfoLoading) return; + emit(UserInfoLoading()); + try { + final response = await AuthRepository.getUserInfo(); + + userInfoModel = response; + userInfoModel.login = true; + emit(UserInfoSuccess()); + } on DioException catch (e) { + if (e.type == DioExceptionType.connectionError || + e.type == DioExceptionType.connectionTimeout) { + emit(UserInfoConnectionError()); + } else if (e.response != null && + e.response!.statusCode != null && + e.response!.statusCode! >= 500) { + emit(UserInfoServerFail()); + } else { + emit(UserInfoFail()); + } + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } + + void changeUser(UserInfoModel newUserInfoModel) { + userInfoModel = newUserInfoModel; + emit(UserInfoSuccess()); + } + + void changeCredit(CreditModel newUserInfoModel) { + final user = userInfoModel; + if (newUserInfoModel.credit != null) { + user.credit = newUserInfoModel.credit; + } + if (newUserInfoModel.freeCredit != null) { + user.freeCredit = newUserInfoModel.freeCredit; + } + userInfoModel = user; + emit(UserInfoSuccess()); + } + + void clearUser() { + userInfoModel = UserInfoModel(); + emit(UserInfoSuccess()); + } +} diff --git a/lib/ui/screens/splash/cubit/user_info_state.dart b/lib/ui/screens/splash/cubit/user_info_state.dart new file mode 100644 index 0000000..37269de --- /dev/null +++ b/lib/ui/screens/splash/cubit/user_info_state.dart @@ -0,0 +1,16 @@ +part of 'user_info_cubit.dart'; + +@immutable +sealed class UserInfoState {} + +final class UserInfoInitial extends UserInfoState {} + +final class UserInfoLoading extends UserInfoState {} + +final class UserInfoSuccess extends UserInfoState {} + +final class UserInfoFail extends UserInfoState {} + +final class UserInfoServerFail extends UserInfoState {} + +final class UserInfoConnectionError extends UserInfoState {} diff --git a/lib/ui/screens/splash/splash_page.dart b/lib/ui/screens/splash/splash_page.dart new file mode 100644 index 0000000..d268c84 --- /dev/null +++ b/lib/ui/screens/splash/splash_page.dart @@ -0,0 +1,215 @@ +// ignore_for_file: unused_local_variable + +import 'package:app_links/app_links.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/core/services/firebase/firebase_api.dart'; +import 'package:hoshan/data/repository/auth_repository.dart'; +import 'package:hoshan/data/storage/shared_preferences_helper.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +class SplashPage extends StatefulWidget { + const SplashPage({super.key}); + + @override + State createState() => _SplashPageState(); +} + +class _SplashPageState extends State { + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + bool hasDeepLink = false; + if (!kIsWeb) { + try { + final appLinks = AppLinks(); + final initialUri = await appLinks.getInitialLink(); + if (initialUri != null && + initialUri.scheme == 'houshan' && + initialUri.host == 'auth') { + final token = initialUri.queryParameters['token']; + if (token != null && token.isNotEmpty) { + if (kDebugMode) { + print('Initial deep link detected: $token'); + } + await AuthTokenStorage.setToken(token); + await OnBoardingStorage.setAsSeen(); + hasDeepLink = true; + if (mounted) { + context.read().getUserInfo(); + } + return; + } + } + } catch (e) { + if (kDebugMode) { + print('Error checking deep link: $e'); + } + } + } + + if (!hasDeepLink && !kIsWeb) { + try { + final packageInfo = await PackageInfo.fromPlatform(); + final currentVersion = packageInfo.version; + final metaData = + await DioService().sendRequest().get('/app-metadata'); + + final Map parsed = { + for (var item in metaData.data) item['tag']: item['value'] + }; + + String latestVersion = parsed['latest_version']; + String message = parsed['message']; + bool forceUpdate = parsed['force'] == 'true'; + + // if (currentVersion != latestVersion) { + // if (mounted) { + // await DialogHandler(context: context).updateAlert( + // message: message, force: forceUpdate, version: latestVersion); + // } + // } + } catch (e) { + if (kDebugMode) { + print('Error is: $e'); + } + } + } + + if (!hasDeepLink) { + String authToken = AuthTokenStorage.getToken(); + if (mounted) { + if (authToken.isEmpty) { + FirebasApi.deleteToken(); + context.go(Routes.auth); + } else { + context.read().getUserInfo(); + } + } + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.primaryColor.defaultShade, + body: BlocConsumer( + listener: (context, state) async { + if (state is UserInfoSuccess) { + if (FirebasApi.fcmToken != null) { + await FirebasApi.refreshToken(); + + await AuthRepository.setFCMtoken(FirebasApi.fcmToken!); + } + if (mounted) { + // ignore: use_build_context_synchronously + context.go(Routes.home); + } + } else if ((state is UserInfoServerFail)) { + SnackBarManager(context, id: 'server-error').show( + message: + 'مشکلی از طرف سرور رخ داده است لطفا لحظاتی دیگر دوباره تلاش کنید', + status: SnackBarStatus.error); + } else if (state is UserInfoFail) { + AuthTokenStorage.clearToken(); + context.go(Routes.auth); + } + }, + builder: (context, state) { + return Stack( + children: [ + Positioned.fill( + top: Responsive(context).isDesktop() ? 90 : 0, + child: (Responsive(context).isMobile() + ? Assets.image.splash.splash + : Assets.image.splash.splashDesk) + .image(fit: BoxFit.contain)), + Positioned.fill( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Flexible(flex: 1, child: SizedBox.shrink()), + Flexible( + flex: 2, + child: Column( + children: [ + const SizedBox( + height: 32, + ), + Assets.icon.launcherIcons.houshanIconWhie + .image(width: 140, height: 140), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Text( + 'هــوشان', + style: AppTextStyles.headline1 + .copyWith(color: Colors.white), + ), + ), + Text( + 'دوست هوش مصنوعی تو', + style: AppTextStyles.body3 + .copyWith(color: Colors.white), + ), + const SizedBox( + height: 46, + ), + state is UserInfoConnectionError || + state is UserInfoServerFail + ? Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0), + child: Center( + child: LoadingButton( + width: (Responsive(context).isMobile() + ? MediaQuery.sizeOf(context) + .width + : 800) / + 2, + height: 46, + radius: 10, + color: AppColors.primaryColor[200], + onPressed: () => context + .read() + .getUserInfo(), + child: Text( + 'تلاش مجدد', + style: AppTextStyles.body4.copyWith( + color: + AppColors.black.defaultShade), + ), + ), + ), + ) + : const CupertinoActivityIndicator( + radius: 24, + color: Colors.white, + ) + ], + )), + const Flexible(flex: 1, child: SizedBox.shrink()), + ], + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/ui/screens/ticket/bloc/send_ticket_bloc.dart b/lib/ui/screens/ticket/bloc/send_ticket_bloc.dart new file mode 100644 index 0000000..0759dee --- /dev/null +++ b/lib/ui/screens/ticket/bloc/send_ticket_bloc.dart @@ -0,0 +1,112 @@ +import 'package:cross_file/cross_file.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/core/utils/date_time.dart'; +import 'package:hoshan/data/model/ticket_model.dart'; +import 'package:hoshan/data/repository/ticket_repository.dart'; +import 'package:persian_number_utility/persian_number_utility.dart'; + +part 'send_ticket_event.dart'; +part 'send_ticket_state.dart'; + +class SendTicketBloc extends Bloc { + SendTicketBloc() : super(SendTicketInitial()) { + on((event, emit) async { + if (event is GetTickets) { + try { + final response = await TicketRepository.getTickets(); + + emit(GetTicketsSuccess(tickets: response.tickets)); + } on DioException catch (e) { + emit(const GetTicketsSuccess(tickets: [])); + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } + if (event is DeleteTicket) { + final List updatedTickets = List.from(state.tickets); + emit(SendTicketLoading(tickets: updatedTickets)); + + try { + await TicketRepository.deleteTickets(event.id); + updatedTickets + .firstWhere( + (element) => element.date == event.date, + ) + .tickets + .removeWhere( + (element) => element.id == event.id, + ); + if (updatedTickets + .firstWhere( + (element) => element.date == event.date, + ) + .tickets + .isEmpty) { + updatedTickets.removeWhere((element) => element.date == event.date); + } + emit(SendTicketSuccess(tickets: updatedTickets)); + } on DioException catch (e) { + emit(GetTicketsSuccess(tickets: updatedTickets)); + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } + if (event is SendTicket) { + // Create a modifiable copy of the tickets list + final List updatedTickets = List.from(state.tickets); + final ticket = Ticket( + id: -1, + text: event.text, + role: 'user', + localFile: event.file, + createdAt: DateTimeUtils.getNow().toIso8601String()); + + if (updatedTickets.isNotEmpty) { + if (updatedTickets.last.tickets.isNotEmpty && + updatedTickets.last.tickets.last.createdAt != null) { + final date = DateTimeUtils.convertStringIsoToDate( + updatedTickets.last.tickets.last.createdAt!); + final now = DateTimeUtils.getNow(); + final dateString = '${date.year}-${date.month}-${date.day}'; + final nowString = '${now.year}-${now.month}-${now.day}'; + if (dateString == nowString) { + updatedTickets.last.tickets.add(ticket); + } else { + updatedTickets.add(Tickets( + date: DateTimeUtils.getNow().toPersianDateStr(), + tickets: [ticket])); + } + } + } else { + updatedTickets.add(Tickets( + date: DateTimeUtils.getNow().toPersianDateStr(), + tickets: [ticket])); + } + + emit(SendTicketLoading(tickets: updatedTickets)); + + try { + final response = await TicketRepository.sendTickets( + text: event.text, file: event.file); + response.localFile = event.file; + updatedTickets.last.tickets.last = response; + emit(SendTicketSuccess(tickets: updatedTickets)); + } on DioException catch (e) { + if (updatedTickets.last.tickets.isNotEmpty) { + updatedTickets.last.tickets.last.id = -2; + } + + emit(GetTicketsSuccess(tickets: updatedTickets)); + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } + }); + } +} diff --git a/lib/ui/screens/ticket/bloc/send_ticket_event.dart b/lib/ui/screens/ticket/bloc/send_ticket_event.dart new file mode 100644 index 0000000..5b25c7d --- /dev/null +++ b/lib/ui/screens/ticket/bloc/send_ticket_event.dart @@ -0,0 +1,25 @@ +part of 'send_ticket_bloc.dart'; + +sealed class SendTicketEvent extends Equatable { + @override + List get props => []; +} + +class GetTickets extends SendTicketEvent {} + +class DeleteTicket extends SendTicketEvent { + final int id; + final String date; + + DeleteTicket({required this.id, required this.date}); +} + +class SendTicket extends SendTicketEvent { + final String text; + final XFile? file; + + SendTicket({required this.text, required this.file}); + + @override + List get props => []; +} diff --git a/lib/ui/screens/ticket/bloc/send_ticket_state.dart b/lib/ui/screens/ticket/bloc/send_ticket_state.dart new file mode 100644 index 0000000..917c51c --- /dev/null +++ b/lib/ui/screens/ticket/bloc/send_ticket_state.dart @@ -0,0 +1,28 @@ +part of 'send_ticket_bloc.dart'; + +sealed class SendTicketState extends Equatable { + final List tickets; + + const SendTicketState({this.tickets = const []}); + + @override + List get props => [tickets]; +} + +final class SendTicketInitial extends SendTicketState {} + +final class GetTicketsSuccess extends SendTicketState { + const GetTicketsSuccess({required super.tickets}); +} + +final class SendTicketLoading extends SendTicketState { + const SendTicketLoading({required super.tickets}); +} + +// final class SendTicketFail extends SendTicketState { +// const SendTicketFail({required super.tickets}); +// } + +final class SendTicketSuccess extends SendTicketState { + const SendTicketSuccess({required super.tickets}); +} diff --git a/lib/ui/screens/ticket/ticket_page.dart b/lib/ui/screens/ticket/ticket_page.dart new file mode 100644 index 0000000..14ab542 --- /dev/null +++ b/lib/ui/screens/ticket/ticket_page.dart @@ -0,0 +1,860 @@ +// ignore_for_file: use_build_context_synchronously, deprecated_member_use_from_same_package + +import 'package:cross_file/cross_file.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/services/file_manager/pick_file_services.dart'; +import 'package:hoshan/core/utils/date_time.dart'; +import 'package:hoshan/core/utils/file.dart'; +import 'package:hoshan/core/utils/strings.dart'; +import 'package:hoshan/data/model/empty_states_enum.dart'; +import 'package:hoshan/data/model/ticket_model.dart'; +import 'package:hoshan/ui/screens/ticket/bloc/send_ticket_bloc.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/audio/player.dart'; +import 'package:hoshan/ui/widgets/components/audio/recorder.dart'; +import 'package:hoshan/ui/widgets/components/button/circle_icon_btn.dart'; +import 'package:hoshan/ui/widgets/components/dialog/bottom_sheets.dart'; +import 'package:hoshan/ui/widgets/components/dialog/dialog_handler.dart'; +import 'package:hoshan/ui/widgets/components/dropdown/more_popup_menu.dart'; +import 'package:hoshan/ui/widgets/components/image/custome_image.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; +import 'package:hoshan/ui/widgets/sections/empty/empty_states.dart'; +import 'package:hoshan/ui/widgets/sections/header/reversible_appbar.dart'; +import 'package:hoshan/ui/widgets/sections/loading/random_container.dart'; + +class TicketPage extends StatefulWidget { + const TicketPage({super.key}); + + @override + State createState() => _TicketPageState(); +} + +class _TicketPageState extends State { + ValueNotifier visibleAttach = ValueNotifier(false); + ValueNotifier visibleRecorder = ValueNotifier(false); + ValueNotifier selectedFile = ValueNotifier(null); + + final TextEditingController message = TextEditingController(); + final GlobalKey containerKey = GlobalKey(); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: ReversibleAppbar( + context, + titleText: 'ارسال تیکت به پشتیبانی', + ), + body: Responsive(context).maxWidthInDesktop( + child: (contxet, maxWidth) => + BlocBuilder( + builder: (context, state) { + if (state is SendTicketInitial) { + return ListView.builder( + shrinkWrap: true, + itemCount: 20, + reverse: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.only(top: 24, bottom: 90), + itemBuilder: (context, index) { + return RandomContainer(isUser: index % 2 == 0); + }, + ); + } + return state.tickets.isEmpty + ? EmptyStates.getEmptyState(status: EmptyStatesEnum.inbox) + : SingleChildScrollView( + reverse: true, + physics: const BouncingScrollPhysics(), + child: ListView.builder( + shrinkWrap: true, + itemCount: state.tickets.length, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.only(top: 24, bottom: 90), + itemBuilder: (context, index) { + final ticketList = state.tickets[index].tickets; + return Column( + children: [ + Row( + children: [ + const Expanded(child: Divider()), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24.0), + child: Text( + state.tickets[index].date ?? '', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + textDirection: TextDirection.rtl, + ), + ), + const Expanded(child: Divider()), + ], + ), + const SizedBox( + height: 8, + ), + ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemCount: ticketList.length, + shrinkWrap: true, + itemBuilder: (context, tIndex) { + Ticket ticket = ticketList[tIndex]; + return chatBubble(ticket, maxWidth, + state.tickets[index].date ?? ''); + }, + ), + ], + ); + }, + ), + ); + }, + ), + ), + bottomSheet: chatBar(), + ); + } + + Container chatBar() { + return Container( + key: containerKey, + color: Theme.of(context).colorScheme.surface, + child: ValueListenableBuilder( + valueListenable: selectedFile, + builder: (context, file, child) { + return ValueListenableBuilder( + valueListenable: visibleRecorder, + builder: (context, inRrecording, child) { + return inRrecording + ? Container( + margin: const EdgeInsets.symmetric( + horizontal: 18.0, vertical: 12), + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).colorScheme.surface), + child: Recorder( + play: true, + onDelete: () { + visibleRecorder.value = false; + selectedFile.value = null; + visibleAttach.value = false; + }, + onRecordFinish: (fileRecorded) { + visibleRecorder.value = false; + visibleAttach.value = false; + + selectedFile.value = fileRecorded; + }, + onError: (p0) { + visibleRecorder.value = false; + selectedFile.value = null; + visibleAttach.value = false; + }, + ), + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + ValueListenableBuilder( + valueListenable: visibleAttach, + builder: (context, show, child) { + return show + ? Container( + margin: const EdgeInsets.fromLTRB( + 32, 24, 16, 24) + .copyWith(bottom: 0), + child: Row( + children: [ + CircleIconBtn( + icon: Assets + .icon.outline.galleryAdd, + color: Theme.of(context) + .colorScheme + .primary, + iconColor: Colors.white, + onTap: () async { + await BottomSheetHandler( + context) + .showPickImage( + onSelect: (file) { + selectedFile.value = file; + + visibleAttach.value = false; + }, + ); + }), + const SizedBox( + width: 8, + ), + CircleIconBtn( + icon: Assets.icon.outline.musicnote, + color: Theme.of(context) + .colorScheme + .primary, + iconColor: Colors.white, + onTap: () async { + final file = + await PickFileService(context) + .getFile( + fileType: + FileType.audio); + if (file != null) { + selectedFile.value = + file.single; + + visibleAttach.value = false; + } + }, + ), + const SizedBox( + width: 8, + ), + CircleIconBtn( + icon: Assets.icon.outline.cardAdd, + color: Theme.of(context) + .colorScheme + .primary, + iconColor: Colors.white, + onTap: () async { + final file = + await PickFileService( + context) + .getFile( + fileType: + FileType.custom, + allowedExtensions: [ + 'pdf', + 'doc', + 'docx', + 'xls', + 'xlsx', + 'xlsm', + 'xlsb', + 'xlt', + 'xltx', + 'xltm' + ]); + if (file != null) { + selectedFile.value = + file.single; + + visibleAttach.value = false; + } + }) + ], + ), + ) + : const SizedBox.shrink(); + }, + ), + if (file != null) + Container( + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 18), + margin: + const EdgeInsets.all(16).copyWith(bottom: 0), + decoration: BoxDecoration( + color: AppColors.primaryColor.defaultShade, + borderRadius: BorderRadius.circular(16) + .copyWith(topRight: Radius.zero)), + child: file.isAudio() + ? Player( + fileUrl: file.path, + inMessages: true, + ) + : Row( + children: [ + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + file.name, + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + )), + const SizedBox( + width: 8, + ), + SizedBox( + width: 46, + child: AspectRatio( + aspectRatio: 3 / 4, + child: ClipRRect( + borderRadius: + BorderRadius.circular(10), + child: file.isImage() + ? GestureDetector( + onTap: () => + DialogHandler( + context: + context) + .showImageHero( + image: file + .path), + child: SizedBox( + child: CustomeImage( + src: file.path, + fit: BoxFit.cover, + )), + ) + : Container( + color: Colors.white, + child: const Icon( + CupertinoIcons.doc)), + ), + ), + ), + ], + ), + ), + Directionality( + textDirection: TextDirection.rtl, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Padding( + padding: + const EdgeInsets.only(bottom: 8.0), + child: + // ValueListenableBuilder( + // valueListenable: message, + // builder: (context, val, _) { + // if (val.text.isEmpty) { + // return CircleIconBtn( + // icon: Assets + // .icon.outline.microphoneChat, + // color: Colors.white, + // onTap: () { + // visibleRecorder.value = true; + // }, + // ); + // } + // return + ValueListenableBuilder( + valueListenable: message, + builder: + (context, controller, _) { + return controller + .text.isEmpty && + file == null + ? CircleIconBtn( + icon: Assets + .icon + .outline + .microphoneChat, + color: + Theme.of(context) + .colorScheme + .primary, + iconColor: + Colors.white, + onTap: () async { + visibleRecorder + .value = true; + }) + : CircleIconBtn( + icon: Assets + .icon.bold.send, + color: Colors.white, + onTap: () async { + //SEND MESSAGE + context + .read< + SendTicketBloc>() + .add(SendTicket( + text: message + .text, + file: selectedFile + .value)); + message.clear(); + selectedFile.value = + null; + }, + ); + }) + // ; + // }), + ), + const SizedBox( + width: 4, + ), + Expanded( + child: ValueListenableBuilder( + valueListenable: message, + builder: (context, val, _) { + return Directionality( + textDirection: + val.text.startsWithEnglish() + ? TextDirection.ltr + : TextDirection.rtl, + child: TextField( + controller: message, + onChanged: (value) {}, + // enabled: , + minLines: 1, + style: AppTextStyles.body4 + .copyWith( + color: + Theme.of(context) + .colorScheme + .onSurface), + maxLines: 6, // Set this + keyboardType: + TextInputType.multiline, + + decoration: InputDecoration( + filled: true, + hintText: + 'بنویسید یا پیام صوتی بگذارید...', + hintStyle: + AppTextStyles.body4, + fillColor: Theme.of(context) + .scaffoldBackgroundColor, + contentPadding: + const EdgeInsets + .fromLTRB( + 18, 12, 18, 12), + border: OutlineInputBorder( + borderSide: + BorderSide.none, + borderRadius: + BorderRadius.circular( + 24), + ), + ), + ), + ); + })), + const SizedBox( + width: 4, + ), + Padding( + padding: + const EdgeInsets.only(bottom: 8.0), + child: CircleIconBtn( + icon: file != null + ? Assets.icon.outline.trash + : Assets.icon.outline.elementPlus, + color: Theme.of(context) + .colorScheme + .primary, + iconColor: Colors.white, + onTap: () { + if (file != null) { + selectedFile.value = null; + return; + } + visibleAttach.value = + !visibleAttach.value; + }, + ), + ), + + /* + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + children: [ + ValueListenableBuilder( + valueListenable: visibleAttach, + builder: (context, isVisible, _) { + return AnimatedVisibility( + isVisible: isVisible, + duration: const Duration( + milliseconds: 200), + child: Padding( + padding: + const EdgeInsets.symmetric( + horizontal: 4.0), + child: Row(children: [ + CircleIconBtn( + icon: Assets.icon.outline + .galleryAdd, + color: Colors.white, + onTap: () async { + await BottomSheetHandler( + context) + .showPickImage( + onSelect: (file) { + selectedFile.value = + file; + }, + ); + }), + Padding( + padding: + const EdgeInsets.only( + right: 4.0), + child: CircleIconBtn( + icon: Assets.icon.outline + .musicnote, + color: Colors.white, + onTap: () async { + final file = + await PickFileService + .getFile( + fileType: + FileType + .audio); + if (file != null) { + selectedFile.value = + file.single; + } + }, + ), + ), + Padding( + padding: + const EdgeInsets.only( + right: 4.0), + child: CircleIconBtn( + icon: Assets.icon + .outline.cardAdd, + color: Colors.white, + onTap: () async { + final file = + await PickFileService.getFile( + fileType: + FileType + .custom, + allowedExtensions: [ + 'pdf' + ]); + if (file != null) { + selectedFile.value = + file.single; + } + }), + ), + ]), + )); + }), + CircleIconBtn( + icon: Assets.icon.outline.elementPlus, + color: Colors.white, + onTap: () => visibleAttach.value = + !visibleAttach.value, + ), + ], + ), + )*/ + ], + )), + ) + ], + ); + }, + ); + }), + ); + } + + Widget chatBubble(Ticket ticket, double maxWidthDesktop, String date) { + final GlobalKey containerKey = GlobalKey(); + + final isUser = ticket.role == 'user'; + XFile? localFile = ticket.localFile; + String? fileUrl = ticket.file; + return GestureDetector( + onLongPress: () { + MorePopupMenuHandler(context: context).showMorePopupMenu( + containerKey: containerKey, + color: isUser + ? AppColors.primaryColor.defaultShade + : Theme.of(context).colorScheme.surface, + items: [ + if (isUser) + PopUpMenuItemModel( + popupMenuItem: PopupMenuItem( + value: 0, + child: MorePopupMenuHandler.morePopUpItem( + color: isUser + ? Colors.white + : Theme.of(context).colorScheme.primary, + icon: Assets.icon.outline.trash, + title: 'حذف')), + click: () { + try { + context + .read() + .add(DeleteTicket(id: ticket.id!, date: date)); + } catch (e) { + if (kDebugMode) { + print("Error when delete message: $e"); + } + } + }, + ), + if (ticket.text != null && ticket.text!.isNotEmpty) + PopUpMenuItemModel( + popupMenuItem: PopupMenuItem( + value: 1, + child: MorePopupMenuHandler.morePopUpItem( + color: isUser + ? Colors.white + : Theme.of(context).colorScheme.primary, + icon: Assets.icon.outline.copy, + title: 'کپی', + ), + ), + click: () async { + await Clipboard.setData( + ClipboardData(text: ticket.text ?? '')); + Future.delayed( + Duration.zero, + () => SnackBarManager(context, id: 'Copy').show( + status: SnackBarStatus.info, + message: 'متن کپی شد 😃', + isTop: false)); + }, + ), + // PopUpMenuItemModel( + // popupMenuItem: PopupMenuItem( + // value: 1, + // child: MorePopupMenuHandler.morePopUpItem( + // icon: Assets.icon.outline.trash, + // title: 'حذف', + // ), + // ), + // click: () async { + // await DialogHandler(context: context).showDeleteItem( + // title: 'پیام مورد نظر پاک شود؟', + // description: '.با این کار اطلاعات شما از بین خواهد رفت', + // onConfirm: () async {}, + // ); + // }, + // ) + ]); + }, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + alignment: isUser ? Alignment.centerRight : Alignment.centerLeft, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + key: containerKey, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: ticket.id == -2 + ? AppColors.red[50] + : isUser + ? AppColors.primaryColor.defaultShade + : Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16).copyWith( + bottomLeft: isUser + ? const Radius.circular(16) + : const Radius.circular(0), + bottomRight: isUser + ? const Radius.circular(0) + : const Radius.circular(16)), + ), + constraints: BoxConstraints( + maxWidth: MediaQuery.sizeOf(context).width * 0.8), + child: Directionality( + textDirection: (ticket.text ?? '').startsWithEnglish() + ? TextDirection.ltr + : TextDirection.rtl, + child: Column( + children: [ + localFile != null + ? Container( + margin: const EdgeInsets.only(bottom: 8), + child: localFile.name.isImage() + ? GestureDetector( + onTap: () => DialogHandler(context: context) + .showImageHero(image: localFile.path), + child: Container( + constraints: BoxConstraints( + maxWidth: + Responsive(context).isMobile() + ? MediaQuery.sizeOf(context) + .width * + 0.5 + : maxWidthDesktop * 0.3, + maxHeight: + Responsive(context).isMobile() + ? MediaQuery.sizeOf(context) + .width * + 0.6 + : maxWidthDesktop * 0.4), + child: ClipRRect( + borderRadius: + BorderRadius.circular(8), + child: CustomeImage( + src: localFile.path, + fit: BoxFit.cover, + )), + ), + ) + : localFile.name.isAudio() + ? Player( + fileUrl: localFile.path, + inMessages: true, + ) + : Container( + decoration: BoxDecoration( + color: AppColors.gray.defaultShade, + borderRadius: + BorderRadius.circular(10)), + padding: const EdgeInsets.all(8), + constraints: + const BoxConstraints(minHeight: 64), + child: Row( + children: [ + SizedBox( + child: localFile.name.isDocument() + ? const Icon( + CupertinoIcons.doc) + : const SizedBox.shrink(), + ), + Expanded( + child: Padding( + padding: + const EdgeInsets.symmetric( + horizontal: 12.0), + child: Text( + localFile.name, + textDirection: localFile.name + .startsWithEnglish() + ? TextDirection.ltr + : TextDirection.rtl, + style: const TextStyle( + fontSize: 16), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + )), + ], + ), + ), + ) + : fileUrl != null + ? Container( + margin: const EdgeInsets.only(bottom: 8), + child: fileUrl.isImage() + ? GestureDetector( + onTap: () => + DialogHandler(context: context) + .showImageHero( + image: fileUrl, + isUrl: true), + child: Container( + constraints: BoxConstraints( + maxWidth: + Responsive(context).isMobile() + ? maxWidthDesktop * 0.5 + : maxWidthDesktop * 0.3, + maxHeight: + Responsive(context).isMobile() + ? maxWidthDesktop * 0.6 + : maxWidthDesktop * 0.4), + child: ClipRRect( + borderRadius: + BorderRadius.circular(8), + child: ImageNetwork( + url: fileUrl, + fit: BoxFit.cover, + )), + ), + ) + : fileUrl.isAudio() + ? Player( + fileUrl: fileUrl, + inMessages: true, + ) + : Container( + decoration: BoxDecoration( + color: + AppColors.gray.defaultShade, + borderRadius: + BorderRadius.circular(10)), + padding: const EdgeInsets.all(8), + constraints: const BoxConstraints( + minHeight: 64), + child: Row( + children: [ + SizedBox( + child: fileUrl.isDocument() + ? const Icon( + CupertinoIcons.doc) + : const SizedBox.shrink(), + ), + Expanded( + child: Padding( + padding: const EdgeInsets + .symmetric( + horizontal: 12.0), + child: Text( + fileUrl.split('/').last, + textDirection: fileUrl + .split('/') + .last + .startsWithEnglish() + ? TextDirection.ltr + : TextDirection.rtl, + style: const TextStyle( + fontSize: 16), + overflow: + TextOverflow.ellipsis, + maxLines: 2, + ), + )), + ], + ), + ), + ) + : const SizedBox.shrink(), + Text( + (ticket.text ?? ''), + style: AppTextStyles.body4.copyWith( + color: isUser + ? Colors.white + : Theme.of(context).colorScheme.onSurface), + ), + ], + ), + ), + ), + if (ticket.createdAt != null) + Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 0, 0), + child: ticket.id == -1 + ? const SizedBox( + width: 12, + height: 12, + child: CircularProgressIndicator()) + : Text( + DateTimeUtils.convertToSentTime(ticket.createdAt!), + style: AppTextStyles.body5 + .copyWith(color: AppColors.gray[700]), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/ui/screens/tools/bloc/tools_bloc.dart b/lib/ui/screens/tools/bloc/tools_bloc.dart new file mode 100644 index 0000000..a120e46 --- /dev/null +++ b/lib/ui/screens/tools/bloc/tools_bloc.dart @@ -0,0 +1,32 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/tools_categories_model.dart'; +import 'package:hoshan/data/repository/bot_repository.dart'; + +part 'tools_event.dart'; +part 'tools_state.dart'; + +class ToolsBloc extends Bloc { + ToolsBloc() : super(ToolsInitial()) { + on((event, emit) async { + if (event is GetAllTools) { + emit(ToolsLoading()); + try { + final response = await BotRepository.getToolsCategories(); + if (response.categories == null || response.categories!.isEmpty) { + emit(ToolsEmpty()); + } else { + emit(ToolsSuccess(categories: response.categories!)); + } + } on DioException catch (e) { + emit(ToolsFail()); + if (kDebugMode) { + print("Dio Error is : $e"); + } + } + } + }); + } +} diff --git a/lib/ui/screens/tools/bloc/tools_event.dart b/lib/ui/screens/tools/bloc/tools_event.dart new file mode 100644 index 0000000..b0701ca --- /dev/null +++ b/lib/ui/screens/tools/bloc/tools_event.dart @@ -0,0 +1,10 @@ +part of 'tools_bloc.dart'; + +sealed class ToolsEvent extends Equatable { + const ToolsEvent(); + + @override + List get props => []; +} + +class GetAllTools extends ToolsEvent {} diff --git a/lib/ui/screens/tools/bloc/tools_state.dart b/lib/ui/screens/tools/bloc/tools_state.dart new file mode 100644 index 0000000..3fcf606 --- /dev/null +++ b/lib/ui/screens/tools/bloc/tools_state.dart @@ -0,0 +1,22 @@ +part of 'tools_bloc.dart'; + +sealed class ToolsState extends Equatable { + final List categories; + + const ToolsState({this.categories = const []}); + + @override + List get props => [categories]; +} + +final class ToolsInitial extends ToolsState {} + +final class ToolsLoading extends ToolsState {} + +final class ToolsSuccess extends ToolsState { + const ToolsSuccess({required super.categories}); +} + +final class ToolsFail extends ToolsState {} + +final class ToolsEmpty extends ToolsState {} diff --git a/lib/ui/screens/tools/single_tool_page.dart b/lib/ui/screens/tools/single_tool_page.dart new file mode 100644 index 0000000..9d5a673 --- /dev/null +++ b/lib/ui/screens/tools/single_tool_page.dart @@ -0,0 +1,131 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/data/model/chat_args.dart'; +import 'package:hoshan/data/model/tools_categories_model.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/bot/bot_grid_card.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:hoshan/ui/widgets/sections/header/reversible_appbar.dart'; + +class SingleToolPage extends StatelessWidget { + final Categories cat; + const SingleToolPage({super.key, required this.cat}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: ReversibleAppbar( + context, + title: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + cat.name ?? '', + style: AppTextStyles.body3 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ), + const SizedBox( + width: 8, + ), + Assets.icon.outline.toolBox.svg( + width: Responsive(context).isMobile() ? null : 32, + color: Theme.of(context).colorScheme.onSurface), + ], + ), + ), + body: Responsive(context).maxWidthInDesktop( + child: (contxet, mw) => SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Directionality( + textDirection: TextDirection.rtl, + child: Column( + children: [ + const SizedBox( + height: 24, + ), + Container( + padding: const EdgeInsets.all(8), + width: 90, + height: 90, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: context.read().isDark() + ? AppColors.black[900] + : AppColors.primaryColor[50], + boxShadow: const [ + BoxShadow( + color: Color(0x664D4D4D), + blurRadius: 6, + offset: Offset(0, 1), + spreadRadius: 0, + ) + ], + ), + child: ImageNetwork( + radius: 16, + // color: Theme.of(context).colorScheme.onSurface, + url: cat.image), + ), + const SizedBox( + height: 8, + ), + Text( + cat.name ?? '', + style: AppTextStyles.headline6 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ), + if (cat.description != null) + Column( + children: [ + const SizedBox( + height: 8, + ), + Text( + cat.description!, + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900]), + ), + ], + ), + const SizedBox( + height: 12, + ), + GridView.builder( + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: Responsive(context).isMobile() ? 2 : 3, + childAspectRatio: 1 / 1, + crossAxisSpacing: 16, + mainAxisSpacing: 16), + shrinkWrap: true, + padding: const EdgeInsets.all(16), + itemCount: cat.bots!.length, + itemBuilder: (context, index) { + return GestureDetector( + onTap: () => context.go(Routes.chatFromSingleTool, + extra: ChatArgs(bot: cat.bots![index])), + child: BotGridCard( + bot: cat.bots![index], + iconic: true, + )); + }, + ) + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/ui/screens/tools/tools_page.dart b/lib/ui/screens/tools/tools_page.dart new file mode 100644 index 0000000..414944b --- /dev/null +++ b/lib/ui/screens/tools/tools_page.dart @@ -0,0 +1,58 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/ui/screens/tools/bloc/tools_bloc.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/widgets/components/bot/tool_card.dart'; +import 'package:hoshan/ui/widgets/components/bot/tool_card_placeholder.dart'; +import 'package:hoshan/ui/widgets/sections/header/reversible_appbar.dart'; + +class ToolsPage extends StatelessWidget { + const ToolsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: ReversibleAppbar( + context, + titleText: 'ابزار ها', + ), + body: Responsive(context).maxWidthInDesktop( + child: (contxet, mw) => Directionality( + textDirection: TextDirection.rtl, + child: BlocBuilder( + builder: (context, state) { + if (state is ToolsFail || state is ToolsEmpty) { + return const SizedBox.shrink(); + } + if (state is ToolsSuccess) { + final privateBots = state.categories; + return ListView.builder( + itemCount: privateBots.length, + shrinkWrap: true, + padding: const EdgeInsets.symmetric(horizontal: 8), + physics: const BouncingScrollPhysics(), + itemBuilder: (context, index) { + final cat = privateBots[index]; + return ToolCard(cat: cat); + }, + ); + } + + return ListView.builder( + itemCount: 10, + shrinkWrap: true, + padding: const EdgeInsets.symmetric(horizontal: 8), + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return const ToolCardPlaceholder(); + }, + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/ui/theme/colors.dart b/lib/ui/theme/colors.dart new file mode 100644 index 0000000..0f8c1a8 --- /dev/null +++ b/lib/ui/theme/colors.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; + +class AppColors { + // Define color shades for different colors + static final ColorShades primaryColor = ColorShades({ + 50: const Color(0xFFE0ECFF), + 100: const Color(0xFFB3C9ED), + 200: const Color(0xFF9CB1D4), + 300: const Color(0xFF88A1CB), + 400: const Color(0xFF6F8EC1), + 500: const Color(0xFF2252A0), // Default + 600: const Color(0xFF1B4280), + 700: const Color(0xFF193B74), + 800: const Color(0xFF143160), + 900: const Color(0xFF0F2548), + }); + + static final ColorShades secondryColor = ColorShades({ + 50: const Color(0xFFF8E7F1), + 100: const Color(0xFFE9B6D3), + 200: const Color(0xFFDA94BC), + 300: const Color(0xFFD27FAE), + 400: const Color(0xFFC9659E), + 500: const Color(0xFFAC1269), // Default + 600: const Color(0xFF920F59), + 700: const Color(0xFF7E0D4D), + 800: const Color(0xFF6F0B44), + 900: const Color(0xFF410728), + }); + + static final ColorShades gray = ColorShades({ + 50: const Color(0xFFF6FCFC), + 100: const Color(0xFFF6F6F6), + 200: const Color(0xFFF2F2F1), + 300: const Color(0xFFECECEB), + 400: const Color(0xFFD9D9DB), + 500: const Color(0xFFE3E2E1), // Default + 600: const Color(0xFFCFCECD), + 700: const Color(0xFFA1A0A0), + 800: const Color(0xFF7d7c7c), + 900: const Color(0xFF5f5f5f), + }); + + static final ColorShades black = ColorShades({ + 50: const Color(0xFFEAEAEA), + 100: const Color(0xFFBDBDBC), + 200: const Color(0xFF9D9D9B), + 300: const Color(0xFF70706E), + 400: const Color(0xFF555451), + 500: const Color(0xFF2A2926), // Default + 600: const Color(0xFF262523), + 700: const Color(0xFF1E1D1B), + 800: const Color(0xFF171715), + 900: const Color(0xFF121110), + }); + + static final ColorShades red = ColorShades({ + 50: const Color(0xFFFCDCD3), + 100: const Color(0xFFBE123C), + 200: const Color(0xFF4C0519), + }); + + static final ColorShades green = ColorShades({ + 50: const Color(0xFFBBF7D0), + 100: const Color(0xFF059669), + 200: const Color(0xFF064E3B), + }); +} + +// Base class for shades +class ColorShades { + final Map _shades; + + ColorShades(this._shades); + + // Default to shade 500 if no shade is specified + Color get defaultShade => _shades[500] ?? _shades[100]!; + + // Define getters for specific shades + Color operator [](int shade) => _shades[shade] ?? _shades[500]!; +} diff --git a/lib/ui/theme/cubit/theme_mode_cubit.dart b/lib/ui/theme/cubit/theme_mode_cubit.dart new file mode 100644 index 0000000..7ff6e3c --- /dev/null +++ b/lib/ui/theme/cubit/theme_mode_cubit.dart @@ -0,0 +1,61 @@ +import 'package:bloc/bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:hoshan/data/storage/shared_preferences_helper.dart'; + +class ThemeModeCubit extends Cubit { + ThemeModeCubit() : super(_themeModeInitial()); + + static ThemeMode _themeModeInitial() { + final mode = ThemeModeStorage.getMode(); + if (mode == 'system') { + final brightness = + WidgetsBinding.instance.platformDispatcher.platformBrightness; + return brightness == Brightness.dark ? ThemeMode.dark : ThemeMode.light; + } else { + if (mode == 'dark') { + return ThemeMode.dark; + } else { + return ThemeMode.light; + } + } + } + + void switchTheme() { + final mode = ThemeModeStorage.getMode(); + if (mode == 'dark') { + ThemeModeStorage.setMode('light'); + emit(ThemeMode.light); + } else { + ThemeModeStorage.setMode('dark'); + emit(ThemeMode.dark); + } + } + + void setDarkMode() { + ThemeModeStorage.setMode('dark'); + emit(ThemeMode.system); + emit(ThemeMode.dark); + } + + void setLightkMode() { + ThemeModeStorage.setMode('light'); + emit(ThemeMode.system); + emit(ThemeMode.light); + } + + void setDefaultSystem() { + final brightness = + WidgetsBinding.instance.platformDispatcher.platformBrightness; + ThemeModeStorage.setMode('system'); + emit(ThemeMode.system); + emit(brightness == Brightness.dark ? ThemeMode.dark : ThemeMode.light); + } + + bool isDark() { + return state == ThemeMode.dark; + } + + String whatIsThemeMode() { + return ThemeModeStorage.getMode(); + } +} diff --git a/lib/ui/theme/my_custom_scroll_behavior.dart b/lib/ui/theme/my_custom_scroll_behavior.dart new file mode 100644 index 0000000..8a87a3c --- /dev/null +++ b/lib/ui/theme/my_custom_scroll_behavior.dart @@ -0,0 +1,11 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +class MyCustomScrollBehavior extends MaterialScrollBehavior { + // Override behavior methods and getters like dragDevices + @override + Set get dragDevices => { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }; +} diff --git a/lib/ui/theme/responsive.dart b/lib/ui/theme/responsive.dart new file mode 100644 index 0000000..2fca6ad --- /dev/null +++ b/lib/ui/theme/responsive.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +class Responsive { + final BuildContext context; + + Responsive(this.context); + + Widget builder( + {required final Widget mobile, + required final Widget desktop, + final Widget? tablet, + final bool tabletAndMobileSame = false}) { + if (isMobile() || (isTablet() && tabletAndMobileSame)) { + return mobile; + } else if (isTablet() && tablet != null) { + return tablet; + } else { + return desktop; + } + } + + Widget maxWidthInDesktop( + {final double maxWidth = 600, + required final Widget Function( + BuildContext contxet, + double mw, + ) child}) { + if (isDesktop() || isTablet()) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxWidth), + child: child(context, maxWidth), + ), + ], + ); + } else { + return child(context, MediaQuery.sizeOf(context).width); + } + } + + bool isMobile() => MediaQuery.sizeOf(context).width < 904; + bool isTablet() => + MediaQuery.sizeOf(context).width < 1280 && + MediaQuery.sizeOf(context).width >= 904; + bool isDesktop() => MediaQuery.sizeOf(context).width >= 1280; +} diff --git a/lib/ui/theme/text.dart b/lib/ui/theme/text.dart new file mode 100644 index 0000000..5a569ba --- /dev/null +++ b/lib/ui/theme/text.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +class AppTextStyles { + static const defaultFontFamily = 'Dana'; + +//Headline + static TextStyle headline1 = const TextStyle( + fontWeight: FontWeight.bold, fontSize: 32, fontFamily: defaultFontFamily); + + static TextStyle headline2 = const TextStyle( + fontWeight: FontWeight.bold, fontSize: 28, fontFamily: defaultFontFamily); + + static TextStyle headline3 = const TextStyle( + fontWeight: FontWeight.bold, fontSize: 26, fontFamily: defaultFontFamily); + + static TextStyle headline4 = const TextStyle( + fontWeight: FontWeight.bold, fontSize: 24, fontFamily: defaultFontFamily); + + static TextStyle headline5 = const TextStyle( + fontWeight: FontWeight.bold, fontSize: 20, fontFamily: defaultFontFamily); + + static TextStyle headline6 = const TextStyle( + fontWeight: FontWeight.bold, fontSize: 18, fontFamily: defaultFontFamily); + +//Headline + +//Body + static TextStyle body1 = const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 20, + fontFamily: defaultFontFamily); + + static TextStyle body2 = const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 20, + fontFamily: defaultFontFamily); + + static TextStyle body3 = const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 18, + fontFamily: defaultFontFamily); + + static TextStyle body4 = const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 16, + fontFamily: defaultFontFamily); + + static TextStyle body5 = const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 14, + fontFamily: defaultFontFamily); + + static TextStyle body6 = const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 12, + fontFamily: defaultFontFamily); + //Body +} diff --git a/lib/ui/theme/theme.dart b/lib/ui/theme/theme.dart new file mode 100644 index 0000000..84f0954 --- /dev/null +++ b/lib/ui/theme/theme.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:hoshan/ui/theme/colors.dart'; + +final ThemeData lightDefaultTheme = ThemeData( + scrollbarTheme: ScrollbarThemeData( + radius: const Radius.circular(100), + thumbColor: WidgetStateProperty.all(AppColors.primaryColor[200]), + trackColor: WidgetStateProperty.all(AppColors.black[50])), + brightness: Brightness.light, + popupMenuTheme: const PopupMenuThemeData( + surfaceTintColor: Colors.transparent, color: Colors.white), + appBarTheme: const AppBarTheme( + surfaceTintColor: Colors.white, backgroundColor: Colors.white), + primaryColor: AppColors.primaryColor.defaultShade, + bottomSheetTheme: const BottomSheetThemeData( + surfaceTintColor: Colors.transparent, backgroundColor: Colors.white), + scaffoldBackgroundColor: AppColors.gray[200], + dialogTheme: const DialogThemeData( + backgroundColor: Colors.white, + surfaceTintColor: Colors.white, + ), + textSelectionTheme: TextSelectionThemeData( + cursorColor: AppColors.secondryColor[200], // Cursor color + selectionColor: AppColors.secondryColor[200], // Highlight color + selectionHandleColor: AppColors.secondryColor[200], // Handle color + ), + colorScheme: ColorScheme.light( + primary: AppColors.primaryColor.defaultShade, + secondary: AppColors.secondryColor.defaultShade, + onSurface: const Color(0xff181818), + surface: Colors.white)); +final ThemeData darkThemeDefault = ThemeData( + scrollbarTheme: ScrollbarThemeData( + radius: const Radius.circular(100), + thumbColor: WidgetStateProperty.all(AppColors.primaryColor.defaultShade), + trackColor: WidgetStateProperty.all(AppColors.black[50])), + brightness: Brightness.dark, + popupMenuTheme: const PopupMenuThemeData( + surfaceTintColor: Colors.transparent, color: Color(0xff292B2E)), + appBarTheme: const AppBarTheme( + surfaceTintColor: Color(0xff1f2123), backgroundColor: Color(0xff1f2123)), + dialogTheme: const DialogThemeData( + backgroundColor: Color(0xff1c1c1c), + surfaceTintColor: Color(0xff1c1c1c), + ), + textSelectionTheme: const TextSelectionThemeData( + cursorColor: Color(0xffed54ab), // Cursor color + selectionColor: Color(0xffed54ab), // Highlight color + selectionHandleColor: Color(0xffed54ab), // Handle color + ), + bottomSheetTheme: const BottomSheetThemeData( + surfaceTintColor: Colors.transparent, backgroundColor: Color(0xff1f2123)), + scaffoldBackgroundColor: const Color(0xff292B2E), + primaryColor: const Color(0xff5f8fdd), + colorScheme: const ColorScheme.dark( + primary: Color(0xff5f8fdd), + secondary: Color(0xffed54ab), + onSurface: Color(0xffe3ecfc), + surface: Color(0xff1f2123)), +); diff --git a/lib/ui/widgets/ai_banner.dart b/lib/ui/widgets/ai_banner.dart new file mode 100644 index 0000000..04575eb --- /dev/null +++ b/lib/ui/widgets/ai_banner.dart @@ -0,0 +1,30 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:hoshan/core/services/webview/webview.dart'; + +class AiBanner extends StatelessWidget { + const AiBanner({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + NativeWebViewLauncher.openWebView( + 'https://www.aisada.ir/app/page1.html'); + }, + child: Padding( + padding: const EdgeInsets.only(top: 20), + child: Stack( + children: [ + SvgPicture.asset('assets/image/boardings/AI Houshan.svg',width: 370,), + Positioned( + left: 15, + child: Image.asset('assets/icon/gif/33.gif',height: 90,)) + ], + ), + ), + ); + } +} diff --git a/lib/ui/widgets/components/animations/animated_visibility.dart b/lib/ui/widgets/components/animations/animated_visibility.dart new file mode 100644 index 0000000..8698aba --- /dev/null +++ b/lib/ui/widgets/components/animations/animated_visibility.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; + +enum FadeMode { + vertical, + horizontal, + both, +} + +class AnimatedVisibility extends StatefulWidget { + final bool isVisible; + final Duration duration; + final Widget child; + final Curve curve; + final FadeMode fadeMode; + + const AnimatedVisibility({ + super.key, + required this.isVisible, + required this.duration, + required this.child, + this.fadeMode = FadeMode.both, + this.curve = Curves.easeIn, + }); + + @override + State createState() => _AnimatedVisibilityState(); +} + +class _AnimatedVisibilityState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _sizeController; + late final Animation _animation; + + @override + void initState() { + super.initState(); + _setupAnimation(); + _runSizeCheck(true); + } + + void _setupAnimation() { + _sizeController = + AnimationController(vsync: this, duration: widget.duration); + _animation = CurvedAnimation( + parent: _sizeController, + curve: widget.curve, + ); + } + + @override + void didUpdateWidget(covariant AnimatedVisibility oldWidget) { + _runSizeCheck(false); + super.didUpdateWidget(oldWidget); + } + + void _runSizeCheck(bool isInit) async { + if (widget.isVisible) { + if (isInit) { + _sizeController.value = 1; + } + _sizeController.forward(); + } else { + _sizeController.reverse(); + } + } + + @override + void dispose() { + _sizeController.dispose(); + super.dispose(); + } + + bool get _isVertical => + widget.fadeMode == FadeMode.vertical || widget.fadeMode == FadeMode.both; + bool get _isHorizontal => + widget.fadeMode == FadeMode.horizontal || + widget.fadeMode == FadeMode.both; + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + child: widget.child, + builder: (context, child) => Align( + heightFactor: _isVertical ? _animation.value : null, + widthFactor: _isHorizontal ? _animation.value : null, + child: Opacity(opacity: _animation.value, child: child), + ), + ); + } +} diff --git a/lib/ui/widgets/components/audio/music_player.dart b/lib/ui/widgets/components/audio/music_player.dart new file mode 100644 index 0000000..b50d689 --- /dev/null +++ b/lib/ui/widgets/components/audio/music_player.dart @@ -0,0 +1,385 @@ +import 'dart:io'; + +import 'package:audioplayers/audioplayers.dart'; +import 'package:background_downloader/background_downloader.dart'; +import 'package:cross_file/cross_file.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/core/services/downloader/downloader_service.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/circle_icon_btn.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:percent_indicator/circular_percent_indicator.dart'; +import 'package:string_validator/string_validator.dart'; + +class MusicPlayer extends StatefulWidget { + final String url; + const MusicPlayer({super.key, required this.url}); + + @override + State createState() => _MusicPlayerState(); +} + +class _MusicPlayerState extends State { + final player = AudioPlayer(); + dynamic fileSource; + bool isReady = false; + Duration? duration; + + final DownloaderService _downloadFileService = DownloaderService(); + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + try { + if (widget.url.isNotEmpty) { + if (!widget.url.isURL()) { + XFile? file = XFile(widget.url); + fileSource = DeviceFileSource(file.path); + } else { + var fileName = widget.url.split('/').last; + if (!fileName.endsWith('.mp3')) { + fileName = '$fileName.mp3'; + } + final directory = await getApplicationDocumentsDirectory(); + final file = File('${directory.path}/$fileName'); + if (await directory.exists() && await file.exists()) { + fileSource = DeviceFileSource(file.path); + } + } + if (fileSource != null) { + try { + await player.setSource(fileSource!); + } catch (e) { + if (kDebugMode) { + print('Error in setSource: $e'); + } + } + duration = await player.getDuration(); + setState(() { + isReady = true; + }); + } + } + } catch (e) { + if (kDebugMode) { + print('Error in initState: $e'); + } + } + }); + } + + @override + void dispose() { + player.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.url.isEmpty) { + return const Text('Audio not found'); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + ValueListenableBuilder( + valueListenable: _downloadFileService.onStatus, + builder: (context, status, _) { + return status == TaskStatus.complete || isReady + ? StreamBuilder( + stream: player.onPlayerStateChanged, + builder: (context, snapshot) { + return CircleIconBtn( + icon: (snapshot.hasData || + !snapshot.hasError) && + snapshot.data == PlayerState.playing + ? Assets.icon.bold.pause + : Assets.icon.bold.play, + color: Theme.of(context).colorScheme.primary, + iconColor: Colors.white, + iconPadding: const EdgeInsets.all(6), + size: 32, + onTap: () async { + if (snapshot.data == PlayerState.playing) { + await player.pause(); + } else if (snapshot.data == + PlayerState.paused) { + await player.resume(); + } else { + if (fileSource != null) { + await player.play(fileSource!); + } + } + }, + ); + }) + : Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.primary), + child: InkWell( + onTap: () async { + if (status == null) { + final path = await _downloadFileService + .downloadFile(widget.url); + if (path != null) { + XFile? file = XFile(path); + fileSource = DeviceFileSource(file.path); + } + if (fileSource != null) { + try { + await player.setSource(fileSource!); + } catch (e) { + if (kDebugMode) { + print('Error in setSource: $e'); + } + } + duration = await player.getDuration(); + setState(() { + isReady = true; + }); + } + } else if (status == TaskStatus.running) { + _downloadFileService.pauseDownload(); + } else { + _downloadFileService.resumeDownload(); + } + }, + child: ValueListenableBuilder( + valueListenable: + _downloadFileService.progressPer, + builder: (context, progress, _) => progress == + 0 && + status != null && + status == TaskStatus.running || + progress < 0 + ? const SizedBox( + width: 28, + height: 28, + child: Padding( + padding: EdgeInsets.all(2.0), + child: Stack( + alignment: Alignment.center, + children: [ + Icon( + Icons.download_rounded, + color: Colors.white, + size: 16, + ), + Positioned.fill( + child: + CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ), + ], + ), + ), + ) + : CircularPercentIndicator( + radius: 14.0, + animation: true, + animationDuration: 100, + lineWidth: 2.0, + percent: progress, + animateFromLastPercent: true, + center: status != null && + status == TaskStatus.running + ? Text( + (progress * 100) + .abs() + .round() + .toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 10.0), + ) + : const Icon( + Icons.download, + color: Colors.white, + size: 14, + ), + circularStrokeCap: + CircularStrokeCap.round, + backgroundColor: AppColors.gray[300], + progressColor: + AppColors.green.defaultShade, + ), + ), + ), + ); + }), + const SizedBox( + width: 8, + ), + InkWell( + onTap: () async { + final pos = await player.getCurrentPosition(); + if (pos != null) { + final targetPosition = + Duration(seconds: pos.inSeconds - 5); + + await player.seek(targetPosition); + } else { + if (kDebugMode) { + print('Failed to get current position'); + } + } + }, + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.primary), + child: const Icon( + Icons.replay_5_rounded, + color: Colors.white, + size: 22, + ), + ), + ), + const SizedBox( + width: 8, + ), + InkWell( + onTap: () async { + final pos = await player.getCurrentPosition(); + if (pos != null) { + final targetPosition = + Duration(seconds: pos.inSeconds + 5); + + await player.seek(targetPosition); + } else { + if (kDebugMode) { + print('Failed to get current position'); + } + } + }, + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.primary), + child: const Icon( + Icons.forward_5_rounded, + color: Colors.white, + size: 22, + ), + ), + ), + ], + ), + InkWell( + onTap: () { + setState(() { + if (player.playbackRate == 2) { + player.setPlaybackRate(1); + } else { + player.setPlaybackRate(2); + } + }); + }, + child: Text( + player.playbackRate == 2 ? '2X' : '1X', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Theme.of(context).colorScheme.primary), + ), + ) + ], + ), + const SizedBox( + height: 16, + ), + if (isReady) + Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (duration != null) + SizedBox( + child: StreamBuilder( + stream: player.onPositionChanged, + builder: (context, position) { + return Slider( + value: position.hasData && !position.hasError + ? position.data!.inMilliseconds.toDouble() + : 0, + onChanged: (value) { + player.seek(Duration(milliseconds: value.round())); + }, + min: 0, + max: duration!.inMilliseconds.toDouble(), + ); + }), + ), + const SizedBox( + height: 8, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + StreamBuilder( + stream: player.onPositionChanged, + builder: (context, snapshot) { + return Text( + snapshot.hasError || + !snapshot.hasData && snapshot.data == null + ? '00:00' + : '${snapshot.data!.inMinutes.remainder(60).toString().padLeft(2, '0')}:${(snapshot.data!.inSeconds.remainder(60)).toString().padLeft(2, '0')}', + style: AppTextStyles.body6.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ); + }), + Text( + duration == null + ? '00:00' + : '${duration!.inMinutes.remainder(60).toString().padLeft(2, '0')}:${(duration!.inSeconds.remainder(60)).toString().padLeft(2, '0')}', + style: AppTextStyles.body6.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + ], + ) + ], + ), + if (widget.url.isURL()) + FutureBuilder( + future: DioService.getFileSize(widget.url), + builder: (context, size) { + if (!size.hasData || size.hasError) { + return const SizedBox.shrink(); + } + return Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(16)), + child: Text( + '${size.data} MB', + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold), + ), + ); + }) + ], + ); + } +} diff --git a/lib/ui/widgets/components/audio/player.dart b/lib/ui/widgets/components/audio/player.dart new file mode 100644 index 0000000..5f85af8 --- /dev/null +++ b/lib/ui/widgets/components/audio/player.dart @@ -0,0 +1,326 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; + +import 'package:audioplayers/audioplayers.dart'; +import 'package:cross_file/cross_file.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/core/utils/date_time.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/circle_icon_btn.dart'; +import 'package:string_validator/string_validator.dart'; + +class Player extends StatefulWidget { + final String fileUrl; + final Function()? onDelete; + final bool inMessages; + const Player( + {super.key, + required this.fileUrl, + this.onDelete, + this.inMessages = false}); + + @override + State createState() => _PlayerState(); +} + +enum DownloadAudioState { onDownloading, downloaded, initial } + +class _PlayerState extends State { + final AudioPlayer audioPlayer = AudioPlayer(); + bool isPlaying = false; + bool isPaused = false; + Duration duration = Duration.zero; + DownloadAudioState downloadState = DownloadAudioState.initial; + StreamSubscription? durationSubscription; + StreamSubscription? positionSubscription; + StreamSubscription? playerStateChangeSubscription; + String? _audioFilePath; + + @override + void initState() { + super.initState(); + try { + setAudio(); + } catch (e) { + if (kDebugMode) { + print("Audio Error is: $e"); + } + } + + playerStateChangeSubscription = audioPlayer.onPlayerStateChanged.listen( + (state) { + if (kDebugMode) { + print("Player state changed: $state"); + } + setState(() { + isPlaying = state == PlayerState.playing; + isPaused = state == PlayerState.paused; + }); + }, + ); + + durationSubscription = audioPlayer.onDurationChanged.listen( + (newDuration) { + if (kDebugMode) { + print("Duration changed: $newDuration"); + } + setState(() { + duration = newDuration; + }); + }, + ); + + audioPlayer.onPlayerComplete.listen((event) { + if (kDebugMode) { + print("Player completed"); + } + }); + + audioPlayer.onLog.listen((msg) { + if (kDebugMode) { + print("AudioPlayer log: $msg"); + } + }); + } + + Future setAudio() async { + try { + audioPlayer.setReleaseMode(ReleaseMode.stop); + + if (widget.fileUrl.isNotEmpty) { + XFile? file; + if (widget.fileUrl.isURL()) { + setState(() { + downloadState = DownloadAudioState.onDownloading; + }); + file = await DioService.downloadFile( + widget.fileUrl, + ); + } else { + file = XFile(widget.fileUrl); + } + if (file != null) { + final fileExists = await File(file.path).exists(); + if (!fileExists) { + if (kDebugMode) { + print("Audio file does not exist: ${file.path}"); + } + setState(() { + downloadState = DownloadAudioState.initial; + }); + return; + } + + await Future.delayed(const Duration(milliseconds: 100)); + + _audioFilePath = file.path; + if (kDebugMode) { + print("Setting audio source: ${file.path}"); + final fileSize = await File(file.path).length(); + print("File size: $fileSize bytes"); + } + try { + await audioPlayer.setSource(DeviceFileSource(file.path)); + setState(() { + downloadState = DownloadAudioState.downloaded; + }); + if (kDebugMode) { + print("Audio source set successfully"); + } + } catch (error) { + if (kDebugMode) { + print("Error setting audio source: $error"); + } + setState(() { + downloadState = DownloadAudioState.initial; + }); + } + } else { + setState(() { + downloadState = DownloadAudioState.initial; + }); + } + } + } catch (e) { + if (kDebugMode) { + print("Error in setAudio: $e"); + } + } + } + + @override + void dispose() { + playerStateChangeSubscription?.cancel(); + durationSubscription?.cancel(); + positionSubscription?.cancel(); + audioPlayer.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + children: [ + if (widget.onDelete != null) + Padding( + padding: const EdgeInsets.only(right: 8), + child: SizedBox( + width: 24, + height: 24, + child: GestureDetector( + onTap: widget.onDelete, + child: Assets.icon.outline.trash.svg(), + ), + ), + ), + widget.inMessages + ? downloadState == DownloadAudioState.initial + ? CircleIconBtn( + icon: Assets.icon.outline.download, + size: 32, + iconPadding: const EdgeInsets.all(6), + iconColor: AppColors.secondryColor.defaultShade, + onTap: () async { + await setAudio(); + }, + ) + : downloadState == DownloadAudioState.onDownloading + ? Stack( + children: [ + Transform.rotate( + angle: pi / 4, + child: CircleIconBtn( + icon: Assets.icon.outline.add, + size: 32, + iconPadding: const EdgeInsets.all(6), + iconColor: AppColors.secondryColor.defaultShade, + onTap: () async {}, + ), + ), + Positioned.fill(child: Builder(builder: (context) { + return const CircularProgressIndicator(); + })) + ], + ) + : CircleIconBtn( + icon: isPlaying + ? Assets.icon.outline.pause + : Assets.icon.outline.play, + size: 32, + iconPadding: const EdgeInsets.all(6), + iconColor: AppColors.secondryColor.defaultShade, + onTap: () async { + try { + if (isPlaying) { + await audioPlayer.pause(); + } else if (isPaused) { + await audioPlayer.resume(); + } else { + if (_audioFilePath != null) { + await audioPlayer + .play(DeviceFileSource(_audioFilePath!)); + } + } + } catch (e) { + if (kDebugMode) { + print("Error playing audio: $e"); + } + } + }, + ) + : SizedBox( + width: 28, + height: 28, + child: GestureDetector( + onTap: () async { + try { + if (isPlaying) { + await audioPlayer.pause(); + } else if (isPaused) { + await audioPlayer.resume(); + } else { + if (_audioFilePath != null) { + await audioPlayer + .play(DeviceFileSource(_audioFilePath!)); + } + } + } catch (e) { + if (kDebugMode) { + print("Error playing audio: $e"); + } + } + }, + child: (isPlaying + ? Assets.icon.bold.pause + : Assets.icon.bold.play) + .svg(color: AppColors.primaryColor.defaultShade), + ), + ), + Expanded( + child: StreamBuilder( + stream: audioPlayer.onPositionChanged, + builder: (context, snapshot) { + Duration position = Duration.zero; + if (snapshot.hasData && snapshot.data != null) { + position = snapshot.data!; + } + return Row( + children: [ + Expanded( + child: Directionality( + textDirection: TextDirection.ltr, + child: Slider( + activeColor: widget.inMessages + ? AppColors.secondryColor[200] + : null, + inactiveColor: AppColors.secondryColor[50], + thumbColor: widget.inMessages + ? AppColors.secondryColor.defaultShade + : null, + min: 0, + max: position < duration + ? duration.inMilliseconds.toDouble() + : 100, + value: position < duration + ? position.inMilliseconds.toDouble() + : 10, + onChanged: (value) async { + if (downloadState != + DownloadAudioState.downloaded) { + return; + } + final position = + Duration(milliseconds: value.toInt()); + await audioPlayer.seek(position); + // await audioPlayer.resume(); + }, + ), + ), + ), + Text( + DateTimeUtils.getTimeFromDuration( + (isPlaying || isPaused ? position : duration) + .inSeconds, + ), + style: AppTextStyles.body4.copyWith( + color: widget.inMessages + ? Colors.white + : Theme.of(context).colorScheme.onSurface), + ), + ], + ); + })), + ], + ), + ); + } +} diff --git a/lib/ui/widgets/components/audio/recorder.dart b/lib/ui/widgets/components/audio/recorder.dart new file mode 100644 index 0000000..a90f8fd --- /dev/null +++ b/lib/ui/widgets/components/audio/recorder.dart @@ -0,0 +1,188 @@ +import 'dart:async'; + +import 'package:cross_file/cross_file.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_sound/flutter_sound.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/services/permission/permission_service.dart'; +import 'package:hoshan/core/utils/date_time.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/audio/player.dart'; +import 'package:hoshan/ui/widgets/components/button/circle_icon_btn.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class Recorder extends StatefulWidget { + final bool play; + final Function(XFile) onRecordFinish; + final Function()? onDelete; + final Function(String?)? onError; + const Recorder( + {super.key, + required this.play, + this.onDelete, + required this.onRecordFinish, + this.onError}); + + @override + State createState() => _RecorderState(); +} + +class _RecorderState extends State { + final recorder = FlutterSoundRecorder(); + bool isRecorderReady = false; + String? path; + Timer? timer; + ValueNotifier seconds = ValueNotifier(0); + + @override + void initState() { + WidgetsBinding.instance.addPostFrameCallback((_) async { + await initRecorder(); + }); + super.initState(); + } + + @override + void dispose() { + super.dispose(); + timer?.cancel(); + } + + Future initRecorder() async { + final status = await PermissionService.getPermission( + permission: Permission.microphone); + if (!status) { + widget.onError?.call('Permission Error'); + throw 'Permission Error'; + } + + await recorder.openRecorder(); + setState(() { + isRecorderReady = true; + }); + recorder.setSubscriptionDuration(const Duration(milliseconds: 500)); + if (widget.play) { + await record(); + } + } + + Future record() async { + if (!isRecorderReady) return; + try { + final tempDir = await getTemporaryDirectory(); + final fileName = '${DateTime.now().millisecondsSinceEpoch ~/ 1000}.aac'; + final filePath = '${tempDir.path}/$fileName'; + + if (kDebugMode) { + print('Starting recording to: $filePath'); + } + + await recorder.startRecorder( + toFile: filePath, + codec: Codec.aacMP4, + ); + timer = Timer.periodic( + const Duration(seconds: 1), + (timer) { + seconds.value = seconds.value + 1; + }, + ); + } catch (e) { + widget.onError?.call('$e'); + + if (kDebugMode) { + print('record Error: $e'); + } + } + } + + Future recordStop() async { + if (!isRecorderReady) return; + timer?.cancel(); + path = await recorder.stopRecorder(); + if (path == null) { + widget.onError?.call('record File Path Error'); + + throw 'record File Path Error'; + } + setState(() {}); + final XFile file = XFile(path!); + widget.onRecordFinish(file); + if (kDebugMode) { + print("filePath: $path"); + } + } + + void handleRecorder() async { + if (recorder.isRecording) { + await recordStop(); + } else { + await record(); + } + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return isRecorderReady + ? SizedBox( + height: 46, + child: path != null + ? Row( + children: [ + Expanded( + child: Player( + fileUrl: path!, + onDelete: widget.onDelete, + )), + ], + ) + : Row( + children: [ + CircleIconBtn( + icon: Assets.icon.bold.stop, + onTap: handleRecorder, + ), + const SizedBox( + width: 8, + ), + Expanded( + child: SizedBox( + height: 28, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + 6, + (index) => SpinKitWave( + color: AppColors.primaryColor.defaultShade, + size: 32, + itemCount: 10, + ), + ), + ), + ), + ), + const SizedBox( + width: 8, + ), + ValueListenableBuilder( + valueListenable: seconds, + builder: (context, s, _) { + return Text( + DateTimeUtils.getTimeFromDuration(s), + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold), + ); + }, + ), + ], + ), + ) + : const SizedBox.shrink(); + } +} diff --git a/lib/ui/widgets/components/bot/bot_grid_card.dart b/lib/ui/widgets/components/bot/bot_grid_card.dart new file mode 100644 index 0000000..4ee7b60 --- /dev/null +++ b/lib/ui/widgets/components/bot/bot_grid_card.dart @@ -0,0 +1,202 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/bot/cubit/bookmark_bot_cubit.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; + +class BotGridCard extends StatelessWidget { + final Bots bot; + final Function(bool)? onMark; + final bool iconic; + final bool showCat; + const BotGridCard( + {super.key, + required this.bot, + this.onMark, + this.iconic = false, + this.showCat = false}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0x664D4D4D), + blurRadius: 6, + offset: Offset(0, 1), + spreadRadius: 0, + ) + ], + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16)), + child: Stack( + children: [ + Positioned( + top: 16, + left: 16, + child: onMark != null + ? BlocProvider( + create: (context) => BookmarkBotCubit() + ..getBotBookMarkStstus(bot.marked ?? false), + child: BlocConsumer( + listener: (context, state) { + if (state is! BookmarkBotLoading && + state is! BookmarkBotInitial) { + onMark?.call(state is BookmarkedBot); + } + }, + builder: (context, state) { + return DefaultPlaceHolder( + enabled: state is! BookmarkedBot && + state is! UnBookMarkedBot, + child: GestureDetector( + onTap: () => context + .read() + .setBotBookMarkStstus(id: bot.id!), + child: (state is BookmarkedBot + ? Assets.icon.bold.archiveTick + : Assets.icon.outline.archiveTick) + .svg( + color: Theme.of(context) + .colorScheme + .primary)), + ); + }, + ), + ) + : const SizedBox.shrink()), + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + const SizedBox( + height: 8, + ), + Center( + child: Container( + padding: const EdgeInsets.all(0.5), + decoration: bot.tool != null && bot.tool! && !iconic + ? BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.primary) + : BoxDecoration( + shape: BoxShape.circle, + color: context.read().isDark() + ? AppColors.black[900] + : AppColors.primaryColor[50], + ), + child: ImageNetwork( + width: 58, + height: 58, + radius: 360, + url: bot.image, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + children: [ + Text( + bot.name ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTextStyles.body5.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface), + ), + const SizedBox( + height: 8, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + mainAxisAlignment: showCat + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.center, + children: [ + if (showCat) + Expanded( + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context) + .colorScheme + .secondary, + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text(bot.category?.name ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTextStyles.body6.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface, + )), + ) + ], + ), + ), + Expanded( + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Assets.icon.outline.coin.svg( + width: 16, + height: 16, + color: Theme.of(context) + .colorScheme + .secondary), + const SizedBox( + width: 4, + ), + Flexible( + child: Text( + bot.cost == 0 + ? 'رایگان' + : showCat + ? bot.cost.toString() + : 'هر پیام ${bot.cost} سکه', + style: AppTextStyles.body5.copyWith( + color: Theme.of(context) + .colorScheme + .secondary), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ) + ], + ), + ), + ], + ), + ), + ], + ), + )), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/widgets/components/bot/bot_grid_card_placeholder.dart b/lib/ui/widgets/components/bot/bot_grid_card_placeholder.dart new file mode 100644 index 0000000..7bc9487 --- /dev/null +++ b/lib/ui/widgets/components/bot/bot_grid_card_placeholder.dart @@ -0,0 +1,95 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter/material.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; + +class BotGridCardPlaceholder extends StatelessWidget { + final int index; + const BotGridCardPlaceholder({super.key, this.index = 0}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0x664D4D4D), + blurRadius: 6, + offset: Offset(0, 1), + spreadRadius: 0, + ) + ], + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + const SizedBox( + height: 8, + ), + Center( + child: DefaultPlaceHolder( + child: Container( + width: 58, + height: 58, + decoration: const BoxDecoration( + shape: BoxShape.circle, color: Colors.white), + ))), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + children: [ + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16)), + child: Text( + 'name of bot', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTextStyles.body3 + .copyWith(fontWeight: FontWeight.bold), + ), + ), + ), + const SizedBox( + height: 8, + ), + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16)), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Assets.icon.outline.clock.svg(width: 16, height: 16), + const SizedBox( + width: 4, + ), + Expanded( + child: Text( + '35 پیام در هر ساعت', + style: AppTextStyles.body5, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ) + ], + ), + ), + ), + ], + ), + )), + ], + ), + ), + ); + } +} diff --git a/lib/ui/widgets/components/bot/bot_row_card.dart b/lib/ui/widgets/components/bot/bot_row_card.dart new file mode 100644 index 0000000..4731af4 --- /dev/null +++ b/lib/ui/widgets/components/bot/bot_row_card.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; + +class BotRowCard extends StatelessWidget { + final Bots bot; + final int index; + const BotRowCard({super.key, required this.bot, this.index = 0}); + + @override + Widget build(BuildContext context) { + final isDark = context.read().state == ThemeMode.dark; + return Container( + width: Responsive(context).isMobile() + ? MediaQuery.sizeOf(context).width * 0.7 + : 300, + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0x664D4D4D), + blurRadius: 6, + offset: Offset(0, 1), + spreadRadius: 0, + ) + ], + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8)), + child: Stack( + children: [ + Positioned( + top: 0, + right: 0, + bottom: 0, + child: Container( + width: 40, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topRight: Radius.circular(8), + bottomRight: Radius.circular(8)), + color: isDark + ? index % 2 == 0 + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.secondary + : index % 2 == 0 + ? AppColors.primaryColor[200] + : AppColors.secondryColor[200]), + )), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + const SizedBox( + width: 8, + ), + Center( + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: index % 2 == 0 + ? AppColors.primaryColor.defaultShade + : AppColors.secondryColor.defaultShade), + child: ImageNetwork( + width: 48, + height: 48, + radius: 360, + url: bot.image, + color: bot.image != null && bot.image!.contains('/llm') + ? Theme.of(context).colorScheme.onSurface + : null, + ), + ), + ), + Expanded( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + bot.name ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTextStyles.body5.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface), + ), + const SizedBox( + height: 4, + ), + Row( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: index % 2 == 0 + ? AppColors.primaryColor.defaultShade + : AppColors.secondryColor.defaultShade, + ), + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 4), + child: Text( + 'رایگان', + style: AppTextStyles.body6.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold), + ), + ), + Container( + height: 12, + width: 1, + color: AppColors.gray.defaultShade, + margin: const EdgeInsets.symmetric(horizontal: 4), + ), + Text( + bot.limit != null + ? bot.limit == -1 + ? 'بدون محدودیت' + : '${bot.limit} پیام در هر ساعت' + : '', + style: AppTextStyles.body5.copyWith( + color: bot.limit == -1 + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).colorScheme.onSurface), + ), + ], + ) + ], + ), + )), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/widgets/components/bot/bot_row_card_placeholder.dart b/lib/ui/widgets/components/bot/bot_row_card_placeholder.dart new file mode 100644 index 0000000..2f7c688 --- /dev/null +++ b/lib/ui/widgets/components/bot/bot_row_card_placeholder.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; + +class BotRowCardPlaceholder extends StatelessWidget { + final int index; + const BotRowCardPlaceholder({super.key, this.index = 0}); + + @override + Widget build(BuildContext context) { + return Container( + width: Responsive(context).isMobile() + ? MediaQuery.sizeOf(context).width * 0.7 + : 300, + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0x664D4D4D), + blurRadius: 6, + offset: Offset(0, 1), + spreadRadius: 0, + ) + ], + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8)), + child: Stack( + children: [ + Positioned( + top: 0, + right: 0, + bottom: 0, + child: DefaultPlaceHolder( + child: Container( + width: 40, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topRight: Radius.circular(8), + bottomRight: Radius.circular(8)), + color: index % 2 == 0 + ? AppColors.primaryColor[50] + : AppColors.secondryColor[50]), + ), + )), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + const SizedBox( + width: 8, + ), + Center( + child: DefaultPlaceHolder( + child: Container( + width: 48, + height: 48, + decoration: const BoxDecoration( + shape: BoxShape.circle, color: Colors.white), + ), + ), + ), + Expanded( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16)), + child: Text( + 'name of bot', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTextStyles.body3 + .copyWith(fontWeight: FontWeight.bold), + ), + ), + ), + const SizedBox( + height: 4, + ), + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16)), + child: Text( + '15 پیام در هر ساعت', + style: AppTextStyles.body5, + ), + ), + ), + ], + ), + )), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/widgets/components/bot/cubit/bookmark_bot_cubit.dart b/lib/ui/widgets/components/bot/cubit/bookmark_bot_cubit.dart new file mode 100644 index 0000000..a9de990 --- /dev/null +++ b/lib/ui/widgets/components/bot/cubit/bookmark_bot_cubit.dart @@ -0,0 +1,30 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/repository/bot_repository.dart'; + +part 'bookmark_bot_state.dart'; + +class BookmarkBotCubit extends Cubit { + BookmarkBotCubit() : super(BookmarkBotInitial()); + + void getBotBookMarkStstus(bool bookMark) { + emit(bookMark ? BookmarkedBot() : UnBookMarkedBot()); + } + + void setBotBookMarkStstus({required final int id}) async { + final oldState = state; + emit(BookmarkBotLoading()); + try { + await BotRepository.botMark( + id: id, marked: oldState is BookmarkedBot ? false : true); + emit(oldState is BookmarkedBot ? UnBookMarkedBot() : BookmarkedBot()); + } on DioException catch (e) { + emit(oldState); + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } +} diff --git a/lib/ui/widgets/components/bot/cubit/bookmark_bot_state.dart b/lib/ui/widgets/components/bot/cubit/bookmark_bot_state.dart new file mode 100644 index 0000000..061e462 --- /dev/null +++ b/lib/ui/widgets/components/bot/cubit/bookmark_bot_state.dart @@ -0,0 +1,16 @@ +part of 'bookmark_bot_cubit.dart'; + +sealed class BookmarkBotState extends Equatable { + const BookmarkBotState(); + + @override + List get props => []; +} + +final class BookmarkBotInitial extends BookmarkBotState {} + +final class BookmarkBotLoading extends BookmarkBotState {} + +final class BookmarkedBot extends BookmarkBotState {} + +final class UnBookMarkedBot extends BookmarkBotState {} diff --git a/lib/ui/widgets/components/bot/tool_card.dart b/lib/ui/widgets/components/bot/tool_card.dart new file mode 100644 index 0000000..0f22cb5 --- /dev/null +++ b/lib/ui/widgets/components/bot/tool_card.dart @@ -0,0 +1,269 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/data/model/tools_categories_model.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; + +class ToolCard extends StatelessWidget { + final Categories cat; + const ToolCard({super.key, required this.cat}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => context.go(Routes.singleTool, extra: cat), + child: Container( + margin: const EdgeInsets.all(8), + constraints: BoxConstraints( + maxWidth: MediaQuery.sizeOf(context).width * + (Responsive(context).isMobile() ? 0.7 : 0.15)), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0x664D4D4D), + blurRadius: 6, + offset: Offset(0, 1), + spreadRadius: 0, + ) + ], + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16)), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(8), + width: 64, + height: 64, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: context.read().isDark() + ? AppColors.black[900] + : AppColors.primaryColor[50] + // boxShadow: const [ + // BoxShadow( + // color: Color(0x664D4D4D), + // blurRadius: 30, + // offset: Offset(0, 1), + // spreadRadius: 0, + // ) + // ], + ), + child: ImageNetwork( + radius: 16, + // color: Theme.of(context).colorScheme.onSurface, + url: cat.image), + ), + const SizedBox( + width: 8, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + cat.name ?? '', + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + const SizedBox( + height: 4, + ), + Row( + children: [ + Assets.icon.outline.coin.svg( + width: 20, + height: 20, + color: Theme.of(context).colorScheme.primary), + Builder(builder: (context) { + if (cat.bots != null && cat.bots!.length == 1) { + return Text( + '${cat.bots!.first.cost} سکه', + style: AppTextStyles.body5.copyWith( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900]), + ); + } + final biggestVal = cat.bots?.fold( + 0, + (previousValue, element) { + try { + if (element.cost! >= previousValue) { + return element.cost!; + } + } catch (e) { + if (kDebugMode) { + print('Error is: $e'); + } + } + + return previousValue; + }, + ); + final smallestVal = cat.bots?.fold( + 9999, + (previousValue, element) { + try { + if (element.cost! <= previousValue) { + return element.cost!; + } + } catch (e) { + if (kDebugMode) { + print('Error is: $e'); + } + } + + return previousValue; + }, + ); + return Text( + smallestVal == 0 && biggestVal == 0 + ? 'رایگان' + : smallestVal == biggestVal + ? '$biggestVal سکه' + : '$smallestVal ${biggestVal != null && biggestVal != 0 ? 'تا $biggestVal' : ''} سکه', + style: AppTextStyles.body5.copyWith( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900]), + ); + }), + ], + ), + ], + ) + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 8), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .secondary + .withAlpha(50), + borderRadius: BorderRadius.circular(8)), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: 4, + height: 4, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.secondary), + ), + const SizedBox( + width: 4, + ), + Text( + '${cat.bots?.length ?? ''} مدل', + style: AppTextStyles.body6.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold), + ) + ], + ), + ), + const SizedBox( + width: 8, + ), + cat.bots != null && cat.bots!.length > 1 + ? Row( + children: [ + ...List.generate( + 2, + (index) { + final yourText = cat.bots![index].name ?? ''; + return Row( + children: [ + Text( + yourText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTextStyles.body6.copyWith( + fontSize: 10, + color: AppColors.gray[context + .read() + .isDark() + ? 600 + : 900]), + ), + const SizedBox( + width: 2, + ), + if (cat.bots!.length != 2 || index != 1) + Text( + ' | ', + style: AppTextStyles.body6.copyWith( + fontSize: 10, + color: AppColors.gray[context + .read() + .isDark() + ? 600 + : 900]), + ), + const SizedBox( + width: 2, + ), + ], + ); + }, + ), + if (cat.bots!.length > 2) + Text( + 'و...', + style: AppTextStyles.body6.copyWith( + fontSize: 10, + color: AppColors.gray[context + .read() + .isDark() + ? 600 + : 900]), + ), + ], + ) + : cat.bots != null && cat.bots!.isNotEmpty + ? Text( + cat.bots!.first.name ?? '', + style: AppTextStyles.body6.copyWith( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900]), + ) + : const SizedBox.shrink(), + ], + )), + const SizedBox( + height: 12, + ) + ], + ), + ), + ); + } +} diff --git a/lib/ui/widgets/components/bot/tool_card_placeholder.dart b/lib/ui/widgets/components/bot/tool_card_placeholder.dart new file mode 100644 index 0000000..f6fa10d --- /dev/null +++ b/lib/ui/widgets/components/bot/tool_card_placeholder.dart @@ -0,0 +1,197 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; + +class ToolCardPlaceholder extends StatelessWidget { + const ToolCardPlaceholder({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.sizeOf(context).width * + (Responsive(context).isMobile() ? 0.7 : 0.15)), + margin: const EdgeInsets.all(8), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0x664D4D4D), + blurRadius: 6, + offset: Offset(0, 1), + spreadRadius: 0, + ) + ], + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16)), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultPlaceHolder( + child: Container( + width: 54, + height: 54, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + // boxShadow: const [ + // BoxShadow( + // color: Color(0x664D4D4D), + // blurRadius: 30, + // offset: Offset(0, 1), + // spreadRadius: 0, + // ) + // ], + ), + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8)), + child: Text( + "cat.name ", + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + ), + ), + const SizedBox( + height: 4, + ), + DefaultPlaceHolder( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8)), + child: Row( + children: [ + Assets.icon.outline.coin.svg( + color: Theme.of(context) + .colorScheme + .primary), + Text( + '', + maxLines: 1, + style: AppTextStyles.body5.copyWith( + color: AppColors.gray[context + .read() + .isDark() + ? 600 + : 900]), + ), + ], + ), + ), + const SizedBox( + height: 4, + ), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8)), + child: Text( + 'سکه مصرفی بر اساس مدل انتخابی', + maxLines: 1, + style: AppTextStyles.body5.copyWith( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900]), + ), + ), + ], + ), + ), + ], + ), + ) + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + DefaultPlaceHolder( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8)), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: 8, + height: 8, + margin: const EdgeInsets.only(bottom: 4), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.secondary), + ), + const SizedBox( + width: 8, + ), + Text( + ' مدل', + style: AppTextStyles.body5.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold), + ) + ], + ), + ), + ), + const SizedBox( + width: 8, + ), + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8)), + child: Text( + 'و... ', + style: AppTextStyles.body6.copyWith( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900]), + ), + ), + ), + ], + )), + const SizedBox( + height: 12, + ) + ], + ), + ); + } +} diff --git a/lib/ui/widgets/components/button/circle_icon_btn.dart b/lib/ui/widgets/components/button/circle_icon_btn.dart new file mode 100644 index 0000000..e1198ad --- /dev/null +++ b/lib/ui/widgets/components/button/circle_icon_btn.dart @@ -0,0 +1,36 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter/material.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/ui/theme/colors.dart'; + +class CircleIconBtn extends StatelessWidget { + final SvgGenImage icon; + final Function()? onTap; + final double size; + final Color? color; + final Color? iconColor; + final EdgeInsetsGeometry? iconPadding; + const CircleIconBtn( + {super.key, + required this.icon, + this.onTap, + this.size = 32, + this.color, + this.iconColor, + this.iconPadding}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + width: size, + height: size, + padding: iconPadding ?? const EdgeInsets.all(5), + decoration: BoxDecoration( + shape: BoxShape.circle, color: color ?? AppColors.gray[400]), + child: icon.svg(color: iconColor)), + ); + } +} diff --git a/lib/ui/widgets/components/button/inventory_btn.dart b/lib/ui/widgets/components/button/inventory_btn.dart new file mode 100644 index 0000000..ecf3503 --- /dev/null +++ b/lib/ui/widgets/components/button/inventory_btn.dart @@ -0,0 +1,55 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/widgets/components/dialog/bottom_sheets.dart'; +import 'package:hoshan/ui/widgets/components/text/credit_cost.dart'; + +class InventoryBtn extends StatelessWidget { + final Bots bot; + const InventoryBtn({super.key, required this.bot}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () async { + await BottomSheetHandler(context).showInventory(bot: bot); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: AppColors.secondryColor[50]), + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Assets.icon.outline.coin.svg(), + const SizedBox( + width: 8, + ), + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: CreditCost( + textColor: Colors.black, + call: false, + loadingColor: Theme.of(context).colorScheme.secondary, + )), + const SizedBox( + width: 8, + ), + Transform.rotate( + angle: 90 * pi / 180, + child: Assets.icon.outline.arrowRight.svg( + color: AppColors.secondryColor.defaultShade, + width: 18, + height: 18)), + ], + ), + ), + ); + } +} diff --git a/lib/ui/widgets/components/button/loading_button.dart b/lib/ui/widgets/components/button/loading_button.dart new file mode 100644 index 0000000..7035ae6 --- /dev/null +++ b/lib/ui/widgets/components/button/loading_button.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; + +class LoadingButton extends StatelessWidget { + final Widget child; + final Function()? onPressed; + final bool loading; + final double? width; + final double? height; + final double radius; + final Color? color; + final Color? backgroundColor; + final bool isOutlined; + const LoadingButton( + {super.key, + required this.child, + this.onPressed, + this.loading = false, + this.width, + this.height, + this.color, + this.radius = 12, + this.isOutlined = false, + this.backgroundColor}); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + SizedBox( + width: width, + height: height, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: isOutlined + ? backgroundColor ?? + Theme.of(context).scaffoldBackgroundColor + : color ?? Theme.of(context).colorScheme.primary, + shape: RoundedRectangleBorder( + side: isOutlined + ? BorderSide( + color: color ?? const Color(0xFF000000), width: 2) + : BorderSide.none, + borderRadius: BorderRadius.circular(radius))), + onPressed: loading ? () {} : onPressed, + child: Opacity(opacity: loading ? 0 : 1, child: child)), + ), + if (loading) + Positioned.fill( + child: Center( + child: SpinKitThreeBounce( + color: isOutlined + ? color ?? Theme.of(context).colorScheme.primary + : Colors.white, + size: height != null ? height! / 2 : 20, + ), + )) + ], + ); + } +} diff --git a/lib/ui/widgets/components/button/tab_btn.dart b/lib/ui/widgets/components/button/tab_btn.dart new file mode 100644 index 0000000..f919d0e --- /dev/null +++ b/lib/ui/widgets/components/button/tab_btn.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/text.dart'; + +class TabBtn extends StatelessWidget { + final String title; + final SvgGenImage icon; + final bool active; + final Function()? click; + const TabBtn( + {super.key, + required this.title, + required this.icon, + required this.active, + this.click}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: click, + child: Column( + children: [ + icon.svg( + color: active + ? Theme.of(context).colorScheme.secondary + : AppColors.gray[ + context.read().isDark() ? 400 : 700]), + Text( + title, + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: active + ? Theme.of(context).colorScheme.secondary + : AppColors.gray[ + context.read().isDark() ? 400 : 700]), + ), + Opacity( + opacity: active ? 1 : 0, + child: Container( + width: double.infinity, + height: 12, + margin: const EdgeInsets.fromLTRB(12, 4, 12, 0), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16))), + ), + ) + ], + ), + ); + } +} diff --git a/lib/ui/widgets/components/calender/persian_date_picker.dart b/lib/ui/widgets/components/calender/persian_date_picker.dart new file mode 100644 index 0000000..7b7e288 --- /dev/null +++ b/lib/ui/widgets/components/calender/persian_date_picker.dart @@ -0,0 +1,363 @@ +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/dropdown/simple_dropdown.dart'; +import 'package:shamsi_date/shamsi_date.dart'; + +class PersianDatePicker extends StatefulWidget { + final double dateHeight; + final Function(List)? onDates; + final Function(List)? onConfirm; + final Function()? onDismise; + final bool hasConfirm; + final bool weekSelect; + final int? dateCounts; + final List? selectedDates; + const PersianDatePicker({ + super.key, + this.dateHeight = 32, + this.onDates, + this.dateCounts, + this.onConfirm, + this.hasConfirm = true, + this.onDismise, + this.selectedDates, + this.weekSelect = false, + }); + + @override + State createState() => _PersianDatePickerState(); +} + +class _PersianDatePickerState extends State { + final Jalali initialDate = Jalali.now(); + final Jalali startDate = Jalali(1390); + late int persianMonthIndex = initialDate.month; + late String persianYearSelected = initialDate.year.toString(); + final CarouselSliderControllerImpl controllerImpl = + CarouselSliderControllerImpl(); + + late final List selectedDates = widget.selectedDates ?? []; + + List persianMonths = [ + "فروردین", + "اردیبهشت", + "خرداد", + "تیر", + "مرداد", + "شهریور", + "مهر", + "آبان", + "آذر", + "دی", + "بهمن", + "اسفند" + ]; + + List daysOfWeek = [ + "شنبه", + "یک شنبه", + "دو شنبه", + "سه شنبه", + "چهار شنبه", + "پنج شنبه", + "جمعه" + ]; + + List days = []; + List getDaysOfMonth() { + final List days = []; + final date = Jalali(int.parse(persianYearSelected), persianMonthIndex); + int monthLength = date.monthLength; + final day = Jalali(int.parse(persianYearSelected), persianMonthIndex, 1); + int index = daysOfWeek.indexOf(day.formatter.wN); + for (var i = 0; i < index; i++) { + days.add(0); + } + for (var i = 1; i <= monthLength; i++) { + days.add(i); + } + this.days = days; + return days; + } + + List years = []; + + @override + void initState() { + super.initState(); + for (var i = 0; i <= initialDate.year - startDate.year; i++) { + years.add((startDate.year + i).toString()); + } + years = years.reversed.toList(); + } + + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.rtl, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'انتخاب تاریخ', + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface), + ), + const SizedBox( + height: 8, + ), + Text( + 'تاریخ روز: ${initialDate.day} ${persianMonths[initialDate.month - 1]} ${initialDate.year}', + style: AppTextStyles.body4.copyWith( + color: AppColors + .gray[context.read().isDark() ? 600 : 900]), + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + Expanded( + child: SimpleDropdown( + initialItem: years.first, + list: years, + onSelect: (selected) { + setState(() { + persianYearSelected = years[selected]; + }); + }, + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: SimpleDropdown( + initialItem: persianMonths[persianMonthIndex - 1], + list: persianMonths, + onSelect: (selected) { + controllerImpl + .animateToPage(selected) + .then((value) => setState(() { + persianMonthIndex = selected + 1; + })); + }, + ), + ), + ], + ), + GridView.builder( + shrinkWrap: true, + itemCount: daysOfWeek.length, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 7, crossAxisSpacing: 16, mainAxisSpacing: 16), + itemBuilder: (context, index) { + return Container( + alignment: Alignment.center, + child: Text( + daysOfWeek[index].split(' ').first, + style: AppTextStyles.body5, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ); + }, + ), + CarouselSlider.builder( + carouselController: controllerImpl, + itemBuilder: (context, index, realIndex) { + return GridView.builder( + shrinkWrap: true, + itemCount: getDaysOfMonth().length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 7, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + mainAxisExtent: widget.dateHeight), + itemBuilder: (context, index) { + return days[index] == 0 + ? const SizedBox() + : GestureDetector( + onTap: () { + setState(() { + final date = Jalali( + int.parse(persianYearSelected), + persianMonthIndex, + days[index]); + if (date > Jalali.now()) return; + + if (widget.weekSelect) { + if (selectedDates.isEmpty) { + selectedDates.add(date); + } else if (selectedDates.length == 7) { + selectedDates.clear(); + selectedDates.add(date); + } else { + final today = Jalali.now(); + if (date == selectedDates.first) { + selectedDates.remove(date); + } else if (date > selectedDates.first) { + if (selectedDates.first.addDays(6) <= + today) { + for (int i = 1; i <= 6; i++) { + selectedDates.add( + selectedDates.first.addDays(i)); + } + } + } else { + for (int i = 1; i <= 6; i++) { + selectedDates + .add(selectedDates.first.addDays(-i)); + } + } + } + } else { + if (selectedDates.contains(date)) { + selectedDates.remove(date); + } else { + if (widget.dateCounts != null && + selectedDates.length == + widget.dateCounts) { + if (selectedDates.length == 1 && + widget.dateCounts == 1) { + selectedDates.clear(); + selectedDates.add(date); + } + return; + } + selectedDates.add(date); + } + } + }); + }, + child: Container( + width: widget.dateHeight, + height: widget.dateHeight, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: + persianMonthIndex == (initialDate.month) && + (days[index]) == initialDate.day && + (selectedDates.contains(Jalali( + int.parse(persianYearSelected), + persianMonthIndex, + days[index]))) + ? Border.all( + width: 2, + color: Theme.of(context) + .colorScheme + .secondary) + : null, + color: persianMonthIndex == + (initialDate.month) && + (days[index]) == initialDate.day + ? context.read().isDark() + ? Theme.of(context) + .colorScheme + .onSurface + .withAlpha(80) + : AppColors.primaryColor[50] + : selectedDates.contains(Jalali( + int.parse(persianYearSelected), + persianMonthIndex, + days[index])) + ? Theme.of(context) + .colorScheme + .secondary + : null), + child: Text( + '${days[index]}', + style: AppTextStyles.body5.copyWith( + color: selectedDates.contains(Jalali( + int.parse(persianYearSelected), + persianMonthIndex, + days[index])) && + !(persianMonthIndex == + (initialDate.month) && + (days[index]) == initialDate.day) + ? Colors.white + : Theme.of(context) + .colorScheme + .onSurface), + ), + ), + ); + }, + ); + }, + itemCount: persianMonths.length, + options: CarouselOptions( + viewportFraction: 1, + initialPage: persianMonthIndex - 1, + disableCenter: false, + enableInfiniteScroll: true, + reverse: false, + autoPlay: false, + autoPlayCurve: Curves.fastOutSlowIn, + enlargeCenterPage: true, + enlargeFactor: 0.3, + height: 6 * widget.dateHeight + 16, + onPageChanged: (index, reason) { + setState(() { + if (reason == CarouselPageChangedReason.manual) { + persianMonthIndex = index + 1; + } + }); + }, + scrollDirection: Axis.horizontal, + ), + ), + if (widget.hasConfirm) + Column( + children: [ + Divider( + color: AppColors.gray.defaultShade, + ), + Padding( + padding: + const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + GestureDetector( + onTap: () { + widget.onDismise?.call(); + }, + child: Text( + 'انصراف', + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900]), + ), + ), + const SizedBox( + width: 24, + ), + GestureDetector( + onTap: () { + widget.onConfirm?.call(selectedDates); + }, + child: Text( + 'تایید', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.primary), + ), + ), + ], + ), + ) + ], + ), + ], + ), + ); + } +} diff --git a/lib/ui/widgets/components/calender/shamsi_year_month_picker.dart b/lib/ui/widgets/components/calender/shamsi_year_month_picker.dart new file mode 100644 index 0000000..2f06603 --- /dev/null +++ b/lib/ui/widgets/components/calender/shamsi_year_month_picker.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/dropdown/simple_dropdown.dart'; +import 'package:shamsi_date/shamsi_date.dart'; + +class ShamsiYearMonthPicker extends StatefulWidget { + final Function(Jalali) onDateSelected; + final Jalali initailDate; + const ShamsiYearMonthPicker( + {super.key, required this.onDateSelected, required this.initailDate}); + + @override + State createState() => _ShamsiYearMonthPickerState(); +} + +class _ShamsiYearMonthPickerState extends State { + late int selectedYear; + late String selectedMonth; + late int selectedMonthNum; + + List years = List.generate(50, (index) => Jalali.now().year - index) + .map((year) => year.toString()) + .toList(); + + List ms = [ + "فروردین", + "اردیبهشت", + "خرداد", + "تیر", + "مرداد", + "شهریور", + "مهر", + "آبان", + "آذر", + "دی", + "بهمن", + "اسفند" + ]; + + late List mounths = [...ms]; + + @override + void initState() { + super.initState(); + final now = widget.initailDate; + if (now.year == Jalali.now().year) { + while (mounths.length == Jalali.now().month + 1) { + mounths.removeLast(); + } + } + selectedYear = now.year; + selectedMonth = now.formatter.mN; + selectedMonthNum = now.month; + + // Ensure initial item matches one of the items in the dropdown list + if (!years.contains(selectedYear.toString())) { + selectedYear = int.parse(years.first); + } + if (!mounths.contains(selectedMonth)) { + selectedMonth = mounths.first; + selectedMonthNum = mounths.indexOf(selectedMonth) + 1; + } + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Year Picker + Row( + children: [ + Expanded( + child: Text( + 'سال', + textAlign: TextAlign.center, + style: AppTextStyles.body2.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold), + )), + Expanded( + child: Text('ماه', + textAlign: TextAlign.center, + style: AppTextStyles.body2.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold))), + ], + ), + const SizedBox( + height: 12, + ), + Row( + children: [ + Expanded( + child: SimpleDropdown( + initialItem: selectedYear.toString(), + list: years, + onSelect: (value) { + setState(() { + selectedYear = int.parse(years[value]); + if (selectedYear == Jalali.now().year) { + while (mounths.length == Jalali.now().month + 1) { + mounths.removeLast(); + } + } else { + mounths = [...ms]; + } + }); + }, + ), + ), + const SizedBox( + width: 24, + ), + Expanded( + child: SimpleDropdown( + initialItem: selectedMonth.toString(), + list: mounths, + onSelect: (value) { + setState(() { + selectedMonth = mounths[value]; + selectedMonthNum = value + 1; + }); + }, + ), + ), + ], + ), + const SizedBox( + height: 12, + ), + + Row( + children: [ + Expanded( + child: LoadingButton( + width: double.infinity, + onPressed: () { + final selectedDate = + Jalali(selectedYear, selectedMonthNum, 1); + widget.onDateSelected(selectedDate); + context.pop(); + }, + color: Theme.of(context).colorScheme.primary, + child: Text( + 'تأیید', + style: AppTextStyles.body4.copyWith( + color: Colors.white, fontWeight: FontWeight.bold), + ), + ), + ), + const SizedBox( + width: 24, + ), + Expanded( + child: LoadingButton( + width: double.infinity, + onPressed: () { + context.pop(); + }, + isOutlined: true, + color: Theme.of(context).colorScheme.primary, + backgroundColor: + Theme.of(context).dialogTheme.backgroundColor, + child: Text( + 'لغو', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold), + )), + ), + ], + ) + ], + ); + } +} diff --git a/lib/ui/widgets/components/chart/custome_line_chart.dart b/lib/ui/widgets/components/chart/custome_line_chart.dart new file mode 100644 index 0000000..f56cf5a --- /dev/null +++ b/lib/ui/widgets/components/chart/custome_line_chart.dart @@ -0,0 +1,180 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; + +class CustomeLineChart extends StatefulWidget { + final List points; + final List dates; + final List titles; + final List values; + + const CustomeLineChart( + {super.key, + required this.points, + required this.dates, + required this.titles, + required this.values}); + + @override + State createState() => _LineChartSample2State(); +} + +class _LineChartSample2State extends State { + List gradientColors = [ + AppColors.secondryColor.defaultShade, + AppColors.primaryColor.defaultShade, + ]; + + bool showAvg = false; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1, + child: IgnorePointer( + ignoring: widget.dates.isEmpty && + widget.titles.isEmpty && + widget.points.isEmpty, + child: Stack( + children: [ + LineChart( + mainData(), + ), + if (widget.dates.isEmpty && + widget.titles.isEmpty && + widget.points.isEmpty) + Positioned.fill( + child: Container( + color: Colors.white.withValues(alpha: 0.4), + child: Center( + child: Text( + 'گزارشی موجود نیست', + style: AppTextStyles.body4, + ), + ), + )) + ], + ), + ), + ); + } + + Widget leftTitleWidgets(String text) { + return SideTitleWidget( + axisSide: AxisSide.bottom, + child: Text(text == '0' ? '' : '$text ', + textDirection: TextDirection.rtl, + style: AppTextStyles.body4.copyWith(fontWeight: FontWeight.bold), + textAlign: TextAlign.left), + ); + } + + LineChartData mainData() { + return LineChartData( + gridData: FlGridData( + show: true, + drawVerticalLine: true, + horizontalInterval: 1, + verticalInterval: 1, + getDrawingHorizontalLine: (value) { + return FlLine( + color: AppColors.gray.defaultShade, + strokeWidth: 1, + ); + }, + getDrawingVerticalLine: (value) { + return FlLine( + color: AppColors.gray.defaultShade, + strokeWidth: 1, + ); + }, + ), + titlesData: FlTitlesData( + show: true, + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + interval: 1, + getTitlesWidget: (value, meta) => leftTitleWidgets( + widget.dates.isEmpty && + widget.titles.isEmpty && + widget.points.isEmpty + ? value.round().toString() + : widget.dates[value.toInt()].toString()), + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + interval: 1, + getTitlesWidget: (value, meta) => leftTitleWidgets( + widget.dates.isEmpty && + widget.titles.isEmpty && + widget.points.isEmpty + ? value.round().toString() + : widget.titles[value.toInt()].toString()), + reservedSize: 30, + ), + ), + ), + borderData: FlBorderData( + show: true, + border: Border.all(color: const Color(0xff37434d)), + ), + minX: 0, + maxX: widget.dates.isEmpty ? 10 : (widget.dates.length - 1).toDouble(), + minY: 0, + maxY: widget.titles.isEmpty ? 10 : (widget.titles.length - 1).toDouble(), + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + tooltipRoundedRadius: 10, + getTooltipItems: (touchedSpots) => List.generate( + touchedSpots.length, + (index) => LineTooltipItem( + widget.values[touchedSpots[index].spotIndex].toString(), + AppTextStyles.body5), + ), + getTooltipColor: (touchedSpot) => Colors.white, + )), + lineBarsData: [ + LineChartBarData( + spots: widget.points, + isCurved: false, + gradient: LinearGradient( + colors: gradientColors, + ), + barWidth: 2, + isStrokeCapRound: true, + dotData: FlDotData( + show: true, + getDotPainter: (p0, p1, p2, p3) => FlDotCirclePainter( + color: gradientColors.first, + strokeColor: Colors.white, + strokeWidth: 2, + radius: 4), + ), + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: + gradientColors.map((color) => color.withAlpha(30)).toList(), + ), + ), + ), + ], + ); + } +} diff --git a/lib/ui/widgets/components/chart/custome_line_chart_placeholder.dart b/lib/ui/widgets/components/chart/custome_line_chart_placeholder.dart new file mode 100644 index 0000000..370e64d --- /dev/null +++ b/lib/ui/widgets/components/chart/custome_line_chart_placeholder.dart @@ -0,0 +1,145 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:hoshan/ui/theme/colors.dart'; + +class CustomeLineChartPlaceholder extends StatefulWidget { + const CustomeLineChartPlaceholder({super.key}); + + @override + State createState() => _LineChartSample2State(); +} + +class _LineChartSample2State extends State { + List gradientColors = [ + AppColors.secondryColor.defaultShade, + AppColors.primaryColor.defaultShade, + ]; + + final limitCount = 100; + final sinPoints = []; + final cosPoints = []; + + double xValue = 0; + double step = 0.08; + + Timer? timer; + + @override + void initState() { + super.initState(); + timer = Timer.periodic(const Duration(milliseconds: 40), (timer) { + while (sinPoints.length > limitCount) { + sinPoints.removeAt(0); + cosPoints.removeAt(0); + } + setState(() { + sinPoints.add(FlSpot(xValue, sin(xValue))); + cosPoints.add(FlSpot(xValue, -sin(xValue))); + }); + xValue += step; + }); + } + + LineChartBarData sinLine(List points) { + return LineChartBarData( + spots: points, + dotData: const FlDotData( + show: false, + ), + gradient: LinearGradient( + colors: gradientColors, + stops: const [0.1, 1.0], + ), + isStrokeCapRound: true, + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: gradientColors.map((color) => color.withAlpha(30)).toList(), + ), + ), + barWidth: 4, + isCurved: true, + ); + } + + @override + void dispose() { + timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1, + child: LineChart( + mainData(), + ), + ); + } + + LineChartData mainData() { + return LineChartData( + minY: -4, + maxY: 4, + minX: sinPoints.first.x, + maxX: sinPoints.last.x, + lineTouchData: const LineTouchData(enabled: false), + clipData: const FlClipData.all(), + lineBarsData: [ + sinLine(sinPoints), + sinLine(cosPoints), + ], + gridData: FlGridData( + show: true, + drawVerticalLine: true, + horizontalInterval: 1, + verticalInterval: 1, + getDrawingHorizontalLine: (value) { + return FlLine( + color: AppColors.gray.defaultShade, + strokeWidth: 1, + ); + }, + getDrawingVerticalLine: (value) { + return FlLine( + color: AppColors.gray.defaultShade, + strokeWidth: 1, + ); + }, + ), + titlesData: FlTitlesData( + show: true, + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: false, + reservedSize: 30, + interval: 1, + getTitlesWidget: (value, meta) => Text(value.toString()), + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: false, + interval: 1, + getTitlesWidget: (value, meta) => Text(value.toString()), + reservedSize: 50, + ), + ), + ), + borderData: FlBorderData( + show: true, + border: Border.all(color: const Color(0xff37434d)), + ), + ); + } +} diff --git a/lib/ui/widgets/components/chart/custome_pi_chart.dart b/lib/ui/widgets/components/chart/custome_pi_chart.dart new file mode 100644 index 0000000..6a72a01 --- /dev/null +++ b/lib/ui/widgets/components/chart/custome_pi_chart.dart @@ -0,0 +1,159 @@ +import 'dart:math'; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:hoshan/data/model/report_model.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/chart/indocator.dart'; + +class CustomePiChart extends StatefulWidget { + final List points; + + const CustomePiChart({super.key, required this.points}); + + @override + State createState() => _CustomePiChartState(); +} + +class _CustomePiChartState extends State { + int touchedIndex = -1; + + bool valid = false; + + late int allPointCoinsUsage = + widget.points.fold(0, (sum, point) => sum + (point.coinUsage ?? 0)); + Color getRandomColor(Set existingColors) { + Random random = Random(); + Color newColor; + + do { + newColor = Color.fromARGB( + 255, // Full opacity + random.nextInt(256), // Red (0-255) + random.nextInt(256), // Green (0-255) + random.nextInt(256), // Blue (0-255) + ); + } while ( + existingColors.contains(newColor)); // Keep generating if color exists + + existingColors.add(newColor); // Add new unique color to the set + return newColor; + } + + late Set usedColors = {}; // Track used colors + + late List colors = List.generate( + widget.points.length, + (index) => getRandomColor(usedColors), + ); + @override + void initState() { + super.initState(); + + for (var report in widget.points) { + if (report.coinUsage != null && report.coinUsage! > 0) { + valid = true; + } + } + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + valid + ? AspectRatio( + aspectRatio: Responsive(context).isDesktop() ? 2 : 1 / 1, + child: Stack( + children: [ + PieChart( + PieChartData( + pieTouchData: PieTouchData( + touchCallback: + (FlTouchEvent event, pieTouchResponse) { + setState(() { + if (!event.isInterestedForInteractions || + pieTouchResponse == null || + pieTouchResponse.touchedSection == null) { + touchedIndex = -1; + return; + } + touchedIndex = pieTouchResponse + .touchedSection!.touchedSectionIndex; + }); + }, + ), + borderData: FlBorderData( + show: false, + ), + sectionsSpace: 4, + centerSpaceRadius: 64, + sections: showingSections(), + ), + ), + Positioned.fill( + child: Center( + child: Text( + 'سکه', + style: AppTextStyles.headline3.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + )) + ], + ), + ) + : const SizedBox( + height: 24, + ), + Directionality( + textDirection: TextDirection.rtl, + child: ListView.builder( + itemCount: widget.points.length, + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Indicator( + count: '${widget.points[index].messagesCount} پیام', + color: colors[index], + text: widget.points[index].date ?? '', + isSquare: false, + ), + ); + }, + ), + ), + ], + ); + } + + List showingSections() { + return List.generate(widget.points.length, (index) { + final isTouched = index == touchedIndex; + final fontSize = isTouched ? 16.0 : 12.0; + final radius = isTouched ? 60.0 : 50.0; + const shadows = [Shadow(color: Colors.black, blurRadius: 2)]; + final point = widget.points[index]; + return PieChartSectionData( + color: colors[index], + value: max((point.coinUsage! * 100) / allPointCoinsUsage, 3.5), + radius: radius, + title: '', + badgeWidget: Text( + '${point.coinUsage}', + textDirection: TextDirection.rtl, + textAlign: TextAlign.center, + style: AppTextStyles.body3.copyWith( + fontSize: fontSize, + fontWeight: FontWeight.bold, + color: Colors.white, + shadows: shadows, + ), + ), + ); + }); + } +} diff --git a/lib/ui/widgets/components/chart/indocator.dart b/lib/ui/widgets/components/chart/indocator.dart new file mode 100644 index 0000000..7130332 --- /dev/null +++ b/lib/ui/widgets/components/chart/indocator.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:hoshan/ui/theme/text.dart'; + +class Indicator extends StatelessWidget { + const Indicator({ + super.key, + required this.color, + required this.text, + required this.count, + required this.isSquare, + this.size = 16, + this.textColor, + }); + final Color color; + final String text; + final String count; + final bool isSquare; + final double size; + final Color? textColor; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + Row( + children: [ + Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: isSquare ? BoxShape.rectangle : BoxShape.circle, + color: color, + ), + ), + const SizedBox( + width: 4, + ), + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + text, + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface), + textDirection: TextDirection.rtl, + ), + ), + ], + ), + const Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0), + child: Opacity(opacity: 0.5, child: Divider()), + )), + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + count, + style: AppTextStyles.body4 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + textDirection: TextDirection.rtl, + ), + ) + ], + ), + ); + } +} diff --git a/lib/ui/widgets/components/custom_bottom_clipper.dart b/lib/ui/widgets/components/custom_bottom_clipper.dart new file mode 100644 index 0000000..9e9774b --- /dev/null +++ b/lib/ui/widgets/components/custom_bottom_clipper.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class CustomBottomClipper extends CustomClipper { + @override + Path getClip(Size size) { + Path path = Path(); + path.lineTo(0, size.height - 40); + path.quadraticBezierTo( + size.width / 2, size.height + 40, size.width, size.height - 40); + path.lineTo(size.width, 0); + path.close(); + return path; + } + + @override + bool shouldReclip(CustomClipper oldClipper) => false; +} diff --git a/lib/ui/widgets/components/dialog/bottom_sheets.dart b/lib/ui/widgets/components/dialog/bottom_sheets.dart new file mode 100644 index 0000000..f09e8cd --- /dev/null +++ b/lib/ui/widgets/components/dialog/bottom_sheets.dart @@ -0,0 +1,897 @@ +// ignore_for_file: deprecated_member_use_from_same_package, use_build_context_synchronously + +import 'package:cross_file/cross_file.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/core/services/file_manager/pick_file_services.dart'; +import 'package:hoshan/core/utils/crop_image.dart'; +import 'package:hoshan/core/utils/date_time.dart'; +import 'package:hoshan/core/utils/strings.dart'; +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/data/model/edittext_state_model.dart'; +import 'package:hoshan/data/model/forum_model.dart'; +import 'package:hoshan/data/model/sort_by_model.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/dialog/dialog_handler.dart'; +import 'package:hoshan/ui/widgets/components/image/custome_image.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; +import 'package:hoshan/ui/widgets/components/text/credit_cost.dart'; +import 'package:hoshan/ui/widgets/components/text/labeled_text_field.dart'; +import 'package:image_cropper/image_cropper.dart'; + +class BottomSheetHandler { + final BuildContext context; + BottomSheetHandler(this.context); + + close(BuildContext c) { + c.pop(); + } + + Future showStringList( + {required final List values, + required final String title, + final Function(String)? onSelect}) async { + final ScrollController scrollController = ScrollController(); + await showModalBottomSheet( + context: context, + backgroundColor: Theme.of(context).colorScheme.surface, + builder: (context) => Container( + width: MediaQuery.sizeOf(context).width, + constraints: + BoxConstraints(maxHeight: MediaQuery.sizeOf(context).height / 2.4), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + const SizedBox( + height: 32, + ), + Text( + title, + style: AppTextStyles.body3.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface), + ), + const SizedBox( + height: 12, + ), + Expanded( + child: Directionality( + textDirection: TextDirection.rtl, + child: Scrollbar( + thumbVisibility: true, + trackVisibility: true, + interactive: true, + controller: scrollController, + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + controller: scrollController, + child: Column( + children: List.generate( + values.length, + (index) => GestureDetector( + onTap: () { + onSelect?.call(values[index]); + close(context); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: Row( + children: [ + Text( + values[index], + style: AppTextStyles.body4.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } + + Future showPickImage( + {final bool withAvatar = false, + final Function(XFile)? onSelect, + final bool profile = false}) async { + await showModalBottomSheet( + context: context, + backgroundColor: Theme.of(context).colorScheme.surface, + builder: (context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 64.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + if (!kIsWeb) + _sqrBtn( + icon: Assets.icon.outline.camera, + title: 'دوربین', + onTap: () async { + try { + XFile? file = await PickFileService.getCameraImage(); + if (file != null) { + file = await CropImage().getCroppedFile( + context: context, + sourcePath: file.path, + aspectRatioPresets: profile + ? CropAspectRatioPreset.square + : null); + if (file != null) { + onSelect?.call(file); + } + close(context); + } + } catch (e) { + if (kDebugMode) { + print('Error Choosing Image: $e'); + } + } + }), + if (!kIsWeb) + const SizedBox( + width: 24, + ), + _sqrBtn( + icon: Assets.icon.outline.galleryAdd, + title: 'گالری', + onTap: () async { + try { + final file = await PickFileService(context) + .getFile(fileType: FileType.image); + if (file != null) { + if (kIsWeb) { + onSelect?.call(file.single); + } else { + final croppedFile = await CropImage().getCroppedFile( + context: context, + sourcePath: file.single.path, + aspectRatioPresets: profile + ? CropAspectRatioPreset.square + : null); + if (croppedFile != null) { + onSelect?.call(XFile(croppedFile.path, + name: file.single.name, + mimeType: file.single.mimeType, + length: await file.single.length(), + lastModified: await file.single.lastModified(), + bytes: await croppedFile.readAsBytes())); + } else { + onSelect?.call(file.single); + } + } + + close(context); + } + } catch (e) { + if (kDebugMode) { + print('Error Choosing Image: $e'); + } + } + }), + // if (withAvatar) + // Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // const SizedBox( + // width: 24, + // ), + // _sqrBtn( + // icon: Assets.icon.outline.emojiHappy, + // title: 'آواتار', + // onTap: () async { + // final file = await PickFileService(context) + // .getFile(fileType: FileType.image); + // if (file != null) { + // onSelect?.call(file.single); + // context.pop(); + // } + // }), + // ], + // ) + ], + ), + ); + }, + ); + } + + Future showIncomeFormula() async { + await showModalBottomSheet( + showDragHandle: true, + backgroundColor: Theme.of(context).colorScheme.surface, + context: context, + builder: (context) { + return SizedBox( + width: MediaQuery.sizeOf(context).width, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + 'فرمول محاسبه درآمد از طریق ساخت دستیار', + style: AppTextStyles.body3.copyWith( + color: context.read().isDark() + ? Theme.of(context).colorScheme.primary + : AppColors.primaryColor[900], + fontWeight: FontWeight.bold), + ), + const SizedBox( + height: 12, + ), + const Divider(), + const SizedBox( + height: 12, + ), + Text( + '10 درصد از کل سکه های مصرف شده توسط دستیار شما', + style: AppTextStyles.body4 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + textDirection: TextDirection.rtl, + ), + ], + ), + ), + ); + }, + ); + } + + Future showInventory({required final Bots bot}) async { + await showModalBottomSheet( + context: context, + backgroundColor: Theme.of(context).colorScheme.surface, + showDragHandle: true, + builder: (context) { + return Container( + width: MediaQuery.sizeOf(context).width, + padding: + const EdgeInsets.symmetric(horizontal: 16).copyWith(bottom: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(0, 24, 0, 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Assets.icon.outline.coin.svg(), + const CreditCost(), + ], + ), + Text( + 'سکه‌های باقی‌مانده', + style: AppTextStyles.body3.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface), + ), + ], + ), + ), + const Divider(), + Container( + alignment: Alignment.centerRight, + margin: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + bot.name ?? '', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + const SizedBox( + width: 8, + ), + ImageNetwork( + url: bot.image, + width: 36, + height: 36, + radius: 360, + color: bot.image != null && bot.image!.contains('/llm') + ? Theme.of(context).colorScheme.onSurface + : null, + ), + ], + ), + ), + Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Assets.icon.outline.coin.svg(), + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + '${bot.cost ?? 0}', + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: + Theme.of(context).colorScheme.onSurface), + ), + ) + ], + ), + Text( + 'سکه برای هر پیام', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + ], + ), + const SizedBox( + height: 16, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + BlocBuilder( + builder: (context, state) { + if (bot.cost != null && bot.cost! > 0) { + final credit = + (UserInfoCubit.userInfoModel.freeCredit ?? 0) + + (UserInfoCubit.userInfoModel.credit ?? 0) + + (UserInfoCubit.userInfoModel.gift_credit ?? + 0); + final count = (credit / bot.cost!).floor(); + return Text( + '$count پیام', + textDirection: TextDirection.rtl, + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: + Theme.of(context).colorScheme.onSurface), + ); + } else { + return Text( + 'نامحدود', + textDirection: TextDirection.rtl, + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: + Theme.of(context).colorScheme.onSurface), + ); + } + }, + ), + Text( + 'پیام های باقی‌مانده', + style: AppTextStyles.body3.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface), + ), + ], + ), + ], + ) + ], + ), + ); + }, + ); + } + + Future showSortBy( + {required final SortByModel initailValue, + required final List items, + Function(SortByModel)? onSelected}) async { + await showModalBottomSheet( + context: context, + backgroundColor: Theme.of(context).colorScheme.surface, + enableDrag: true, + showDragHandle: true, + useSafeArea: true, + builder: (context) { + return Container( + width: MediaQuery.sizeOf(context).width, + padding: + const EdgeInsets.symmetric(horizontal: 16).copyWith(bottom: 16), + child: Directionality( + textDirection: TextDirection.rtl, + child: ListView.builder( + itemCount: items.length, + shrinkWrap: true, + itemBuilder: (context, index) { + final item = items[index]; + return GestureDetector( + onTap: () { + onSelected?.call(item); + close(context); + }, + child: Row( + children: [ + Checkbox( + value: initailValue.value == item.value, + onChanged: (val) { + onSelected?.call(item); + close(context); + }), + const SizedBox( + width: 4, + ), + Text( + item.text, + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ) + ], + ), + ); + }, + ), + ), + ); + }, + ); + } + + Future showAddComment( + {final Function(String, XFile?)? onSend, final Comment? comment}) async { + ValueNotifier file = ValueNotifier(null); + ValueNotifier text = ValueNotifier(''); + ValueNotifier loading = ValueNotifier(false); + final EdittextStateModel stateModel = EdittextStateModel( + label: 'متن ${comment == null ? 'سوال' : 'پاسخ'}', + hintText: '${comment == null ? 'سوال' : 'پاسخ'} خود را بنویسید...', + ); + + int? daysAgo; + if (comment != null) { + daysAgo = DateTimeUtils.getDaysBetweenNowAnd(comment.createdAt!); + } + + await showModalBottomSheet( + context: context, + backgroundColor: Theme.of(context).colorScheme.surface, + isScrollControlled: true, + enableDrag: true, + showDragHandle: true, + useSafeArea: true, + constraints: BoxConstraints( + minHeight: MediaQuery.sizeOf(context).height * 0.8, + maxHeight: double.infinity), + builder: (context) { + return Responsive(context).maxWidthInDesktop( + maxWidth: 800, + child: (contxet, mw) => Container( + width: mw, + padding: + const EdgeInsets.symmetric(horizontal: 16).copyWith(bottom: 16), + child: SingleChildScrollView( + child: ValueListenableBuilder( + valueListenable: loading, + builder: (context, wait, _) { + return Column( + children: [ + if (comment != null) + Container( + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.only(bottom: 24), + decoration: BoxDecoration( + border: Border.all( + color: AppColors.gray.defaultShade), + borderRadius: BorderRadius.circular(10)), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + comment.createdAt != null + ? Row( + children: [ + Assets.icon.outline.clock.svg( + width: 20, + height: 20, + color: AppColors + .gray[context + .read< + ThemeModeCubit>() + .isDark() + ? 600 + : 900]), + const SizedBox( + width: 8, + ), + Text( + daysAgo == 0 + ? 'امروز' + : ('$daysAgo روز پیش'), + style: AppTextStyles.body5 + .copyWith( + color: Theme.of( + context) + .colorScheme + .onSurface), + textDirection: + TextDirection.rtl, + ) + ], + ) + : const SizedBox.shrink(), + Row( + children: [ + Text( + comment.user?.username ?? '', + style: AppTextStyles.body4 + .copyWith( + fontWeight: + FontWeight.bold, + color: Theme.of(context) + .colorScheme + .onSurface), + ), + const SizedBox( + width: 8, + ), + ], + ) + ], + ), + const SizedBox( + height: 8, + ), + SizedBox( + width: MediaQuery.sizeOf(context).width, + child: Text( + comment.text ?? '', + style: AppTextStyles.body3.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + textDirection: comment.text != null && + comment.text! + .startsWithEnglish() + ? TextDirection.ltr + : TextDirection.rtl, + ), + ), + const SizedBox( + height: 8, + ), + if (comment.image != null) + Column( + children: [ + Container( + margin: + const EdgeInsets.symmetric( + vertical: 12), + constraints: BoxConstraints( + maxHeight: + MediaQuery.sizeOf(context) + .height * + 0.2), + alignment: Alignment.centerRight, + child: AspectRatio( + aspectRatio: 16 / 9, + child: ImageNetwork( + url: DioService.baseURL + + comment.image!, + radius: 16, + showHero: true, + ), + ), + ), + const SizedBox( + height: 8, + ), + ], + ), + ], + ), + ), + const SizedBox( + width: 12, + ), + ImageNetwork( + url: comment.user != null && + comment.user!.image != null + ? DioService.baseURL + + comment.user!.image! + : 'err', + width: 40, + height: 40, + radius: 360, + ) + ], + ), + ), + LabeledTextField( + stateController: stateModel, + maxLines: 6, + minLines: 6, + enabled: !wait, + onChange: (value) { + text.value = value; + }, + ), + const SizedBox( + height: 12, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ValueListenableBuilder( + valueListenable: text, + builder: (context, message, _) { + return LoadingButton( + loading: wait, + onPressed: message.isNotEmpty + ? () async { + loading.value = true; + await onSend?.call( + message, file.value); + loading.value = false; + } + : null, + color: + AppColors.green.defaultShade, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Transform.flip( + flipX: true, + child: Assets.icon.bold.send + .svg( + color: + Colors.white), + ), + const SizedBox( + width: 8, + ), + Text( + 'انتشار', + style: AppTextStyles.body3 + .copyWith( + color: Colors.white, + fontWeight: + FontWeight + .bold), + ), + ], + )); + }), + ], + ), + const SizedBox( + height: 12, + ), + ValueListenableBuilder( + valueListenable: file, + builder: (context, value, child) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + LoadingButton( + loading: wait, + onPressed: () async { + if (value != null) { + file.value = null; + return; + } + file.value = + (await PickFileService( + context) + .getFile( + fileType: FileType + .image)) + ?.single; + }, + color: value != null + ? AppColors.red.defaultShade + : AppColors + .primaryColor.defaultShade, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + value == null + ? 'اضافه کردن عکس' + : 'حذف عکس', + style: AppTextStyles.body3 + .copyWith( + color: Colors.white, + fontWeight: + FontWeight.bold), + ), + const SizedBox( + width: 8, + ), + value != null + ? Assets.icon.outline.trash + .svg( + color: Colors.white) + : Assets + .icon.outline.galleryAdd + .svg( + color: Colors.white) + ], + )), + ], + ); + }, + ), + ], + ), + const SizedBox( + height: 12, + ), + ValueListenableBuilder( + valueListenable: file, + builder: (context, value, child) { + if (value != null) { + return Padding( + padding: const EdgeInsets.only(right: 8.0), + child: GestureDetector( + onTap: () => + DialogHandler(context: context) + .showImageHero(image: value.path), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: AspectRatio( + aspectRatio: 16 / 9, + child: CustomeImage( + src: value.path, + fit: BoxFit.cover, + ), + ), + ), + ), + ); + } + + return const SizedBox.shrink(); + }, + ), + ], + ) + ], + ); + }), + ), + ), + ); + }, + ); + } + + Widget _sqrBtn( + {required final SvgGenImage icon, + final String? title, + final Function()? onTap}) { + final isDark = context.read().state == ThemeMode.dark; + return GestureDetector( + onTap: onTap, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 64, + height: 64, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: isDark + ? Theme.of(context).colorScheme.onSurface + : AppColors.gray[400]), + child: icon.svg(color: AppColors.black.defaultShade)), + if (title != null) + Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 4, + ), + Text( + title, + style: AppTextStyles.body3.copyWith( + color: + isDark ? Colors.white : AppColors.primaryColor[800]), + ) + ], + ) + ], + ), + ); + } + + Future showReportOptions() async { + await showModalBottomSheet( + context: context, + backgroundColor: Theme.of(context).colorScheme.surface, + showDragHandle: true, + useSafeArea: true, + builder: (context) { + final textStyle = AppTextStyles.body4 + .copyWith(color: Theme.of(context).colorScheme.onSurface); + final options = [ + 'محتوای توهین‌آمیز یا نفرت‌پراکن', + 'اسپم یا تبلیغات نامربوط', + 'اطلاعات نادرست یا گمراه‌کننده', + 'نقض حریم خصوصی' + ]; + return Directionality( + textDirection: TextDirection.rtl, + child: Column( + children: [ + ...List.generate( + options.length, + (index) { + return Column( + children: [ + InkWell( + onTap: () { + context.pop(); + SnackBarManager(context, id: 'report-success').show( + status: SnackBarStatus.success, + message: 'گزارش با موفقیت ارسال شد'); + }, + child: ListTile( + title: Text( + options[index], + style: textStyle, + ), + ), + ), + const Divider(), + ], + ); + }, + ), + InkWell( + onTap: () { + DialogHandler(context: context).showCustomeReport(); + }, + child: ListTile( + title: Text('دیگر (لطفاً توضیح دهید)', style: textStyle), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/ui/widgets/components/dialog/dialog_handler.dart b/lib/ui/widgets/components/dialog/dialog_handler.dart new file mode 100644 index 0000000..1b1c5f4 --- /dev/null +++ b/lib/ui/widgets/components/dialog/dialog_handler.dart @@ -0,0 +1,2044 @@ +// ignore_for_file: deprecated_member_use_from_same_package, use_build_context_synchronously, deprecated_member_use + +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/core/services/firebase/firebase_api.dart'; +import 'package:hoshan/data/model/ai/credit_model.dart'; +import 'package:hoshan/data/model/chat_args.dart'; +import 'package:hoshan/data/model/event_model.dart'; +import 'package:hoshan/data/storage/shared_preferences_helper.dart'; +import 'package:hoshan/ui/screens/main/home/bloc/bots_bloc.dart'; +import 'package:hoshan/ui/screens/setting/cubit/settlement_cubit.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/calender/persian_date_picker.dart'; +import 'package:hoshan/ui/widgets/components/calender/shamsi_year_month_picker.dart'; +import 'package:hoshan/ui/widgets/components/image/custome_image.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; +import 'package:hoshan/ui/widgets/components/text/auth_text_field.dart'; +import 'package:hoshan/ui/widgets/components/video/video_player_widget.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:shamsi_date/shamsi_date.dart'; +import 'package:string_validator/string_validator.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class DialogHandler { + final BuildContext context; + final double maxDialogWidthInDesktop = 600; + + DialogHandler({required this.context}); + + Future loadingDialog({final String? text}) async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Responsive(context).maxWidthInDesktop( + maxWidth: maxDialogWidthInDesktop, + child: (contxet, maxWidth) => Dialog( + shadowColor: Colors.white, + backgroundColor: Colors.white, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: AspectRatio( + aspectRatio: 1 / 1, + child: Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: SpinKitThreeBounce( + size: 36, + color: AppColors.primaryColor.defaultShade, + ), + ), + ), + ), + ), + ), + ); + } + + Future showDatePicker( + {final Function(List)? onConfirm, + final Function()? onDismise, + final int? dateCounts, + final List? selectedDates}) async { + await showDialog( + context: context, + builder: (context) { + return Responsive(context).maxWidthInDesktop( + maxWidth: maxDialogWidthInDesktop, + child: (contxet, maxWidth) => Dialog( + surfaceTintColor: Theme.of(context).colorScheme.surface, + backgroundColor: Theme.of(context).colorScheme.surface, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: MediaQuery.sizeOf(context).width, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16), + ), + child: PersianDatePicker( + selectedDates: selectedDates, + dateCounts: dateCounts, + onConfirm: (p0) { + onConfirm?.call(p0); + context.pop(); + }, + onDismise: () { + onDismise?.call(); + + context.pop(); + }, + ), + ), + ], + )), + ); + }, + ); + } + + Future showExit() async { + bool exit = false; + await showDialog( + context: context, + builder: (context) { + return Responsive(context).maxWidthInDesktop( + maxWidth: maxDialogWidthInDesktop, + child: (contxet, maxWidth) => Dialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + margin: const EdgeInsets.all(16), + width: 120, + height: 120, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + shape: BoxShape.circle, color: AppColors.red[50]), + child: Assets.icon.gif.exit.image()), + Text( + 'خروج از برنامه', + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context).colorScheme.onSurface), + textAlign: TextAlign.center, + ), + const SizedBox( + height: 4, + ), + Text( + 'میخواهید از برنامه خارج شوید؟', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface), + textAlign: TextAlign.center, + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: LoadingButton( + width: MediaQuery.sizeOf(context).width, + radius: 32, + isOutlined: true, + color: Theme.of(context).colorScheme.secondary, + backgroundColor: + Theme.of(context).dialogTheme.backgroundColor, + onPressed: () { + exit = true; + context.pop(); + }, + child: Text( + 'بله', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.secondary), + ), + ), + )), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: LoadingButton( + width: MediaQuery.sizeOf(context).width, + radius: 32, + color: Theme.of(context).colorScheme.secondary, + child: Text('خیر', + style: AppTextStyles.body4 + .copyWith(color: Colors.white)), + onPressed: () { + context.pop(); + }, + ), + )), + ], + ) + ], + ), + ), + ), + ); + }, + ); + return exit; + } + + Future showLogOut() async { + await showDialog( + context: context, + builder: (context) { + return Responsive(context).maxWidthInDesktop( + maxWidth: maxDialogWidthInDesktop, + child: (contxet, maxWidth) => Dialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + margin: const EdgeInsets.all(16), + width: 120, + height: 120, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + shape: BoxShape.circle, color: AppColors.red[50]), + child: Assets.icon.gif.exit.image()), + Text( + '!خروج از حساب کاربری', + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context).colorScheme.onSurface), + textAlign: TextAlign.center, + ), + const SizedBox( + height: 4, + ), + Text( + 'از خروج اطمینان دارید؟', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface), + textAlign: TextAlign.center, + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: LoadingButton( + width: MediaQuery.sizeOf(context).width, + radius: 32, + isOutlined: true, + backgroundColor: + Theme.of(context).dialogTheme.backgroundColor, + color: Theme.of(context).colorScheme.secondary, + onPressed: () { + AuthTokenStorage.clearToken(); + FirebasApi.deleteToken(); + context.read().clearUser(); + + contxet.go(Routes.main, extra: false); + }, + child: Text( + 'بله', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.secondary), + ), + ), + )), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: LoadingButton( + width: MediaQuery.sizeOf(context).width, + radius: 32, + color: Theme.of(context).colorScheme.secondary, + child: Text('خیر', + style: AppTextStyles.body4 + .copyWith(color: Colors.white)), + onPressed: () { + context.pop(); + }, + ), + )), + ], + ) + ], + ), + ), + ), + ); + }, + ); + } + + Future showDeleteItem( + {final String? title, + final String? description, + final Function()? onConfirm}) async { + await showDialog( + context: context, + builder: (context) { + return Responsive(context).maxWidthInDesktop( + maxWidth: maxDialogWidthInDesktop, + child: (contxet, maxWidth) => Dialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + margin: const EdgeInsets.all(16), + width: 120, + height: 120, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + shape: BoxShape.circle, color: AppColors.red[50]), + child: Assets.icon.gif.delete.image()), + if (title != null) + Text( + title, + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context).colorScheme.onSurface), + textAlign: TextAlign.center, + textDirection: TextDirection.rtl, + ), + const SizedBox( + height: 4, + ), + if (description != null) + Text( + description, + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface), + textAlign: TextAlign.center, + textDirection: TextDirection.rtl, + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: LoadingButton( + width: MediaQuery.sizeOf(context).width, + radius: 32, + isOutlined: true, + backgroundColor: + Theme.of(context).dialogTheme.backgroundColor, + color: Theme.of(context).colorScheme.secondary, + onPressed: () { + onConfirm?.call(); + context.pop(); + }, + child: Text( + 'بله', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.secondary), + ), + ), + )), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: LoadingButton( + width: MediaQuery.sizeOf(context).width, + radius: 32, + color: Theme.of(context).colorScheme.secondary, + child: Text('خیر', + style: AppTextStyles.body4 + .copyWith(color: Colors.white)), + onPressed: () { + context.pop(); + }, + ), + )), + ], + ) + ], + ), + ), + ), + ); + }, + ); + } + + Future showImageHero( + {required final String image, final bool? isUrl}) async { + final TransformationController transformationController = + TransformationController(); + await showDialog( + context: context, + builder: (context) { + return Responsive(context).maxWidthInDesktop( + maxWidth: maxDialogWidthInDesktop, + child: (contxet, maxWidth) => Dialog( + insetPadding: EdgeInsets.zero, + backgroundColor: Colors.transparent, + child: SizedBox( + width: double.infinity, + height: double.infinity, + child: GestureDetector( + onTap: () => context.pop(), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: InteractiveViewer( + transformationController: transformationController, + panEnabled: true, + minScale: 0.5, + maxScale: 4, + child: Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: GestureDetector( + onTap: () {}, + onDoubleTapDown: (details) { + final double currentScale = transformationController + .value + .getMaxScaleOnAxis(); + final double newScale = + currentScale >= 4.0 ? 1 : currentScale * 2; + + final Offset tapPosition = details.localPosition; + + final Matrix4 newMatrix = Matrix4.identity() + ..scale(newScale) + ..translate( + (newScale == 1 ? 1 : tapPosition.dx) * + (1 - newScale / currentScale), + (newScale == 1 ? 1 : tapPosition.dy) * + (1 - newScale / currentScale), + ); + + transformationController.value = newMatrix; + }, + child: Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.sizeOf(context).width * 0.8, + maxHeight: MediaQuery.sizeOf(context).width * 0.8, + ), + child: isUrl ?? image.isURL() + ? ImageNetwork( + url: image, + radius: 4, + ) + : ClipRRect( + borderRadius: BorderRadius.circular(4), + child: CustomeImage( + src: image, + )), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ); + }, + ); + } + + Future showVideoHero( + {required final String url, final bool? isUrl}) async { + await showDialog( + context: context, + builder: (context) { + return Responsive(context).maxWidthInDesktop( + maxWidth: maxDialogWidthInDesktop, + child: (contxet, maxWidth) => Dialog( + insetPadding: EdgeInsets.zero, + backgroundColor: Colors.transparent, + child: Stack( + children: [ + SizedBox( + width: double.infinity, + height: double.infinity, + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: VideoPlayerWidget( + url: url, + )), + ), + Positioned( + top: 16, + left: 16, + child: GestureDetector( + onTap: () => context.pop(), + child: Container( + width: 24, + height: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Theme.of(context).colorScheme.onSurface, + )), + child: Icon( + CupertinoIcons.xmark, + size: 18, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + )) + ], + ), + ), + ); + }, + ); + } + + void showUpgradeCredit() async { + await showAlertDialog( + title: 'اوه نه! اعتبارت تموم شد!', + imageGif: Assets.icon.gif.oneCoin.image(), + description: + 'برای ادامه ماجراجویی‌هاتون با مدل‌های هوش مصنوعی حرفه‌ای، لطفاً اشتراکتون رو ارتقا بدین!', + onConfirmText: 'ارتقای اشتراک', + onConfirm: (c) async { + c.go(Routes.purchase); + }, + ); + } + + void showInstagramFollow({ + final Function()? onConfirm, + final Function()? onCancel, + }) async { + await showAlertDialog( + title: '!هوشانو دنبال کن', + imageGif: Assets.icon.gif.instagram.image(), + description: + 'اگه دنبال آخرین اخبار، آموزش‌ها، کدهای تخفیف و کلی چیز خفن دیگه هستی، حتماً ما رو تو اینستاگرام فالو کن! تازه، 20 سکه هوشانی هم هدیه می‌گیری!', + onConfirmText: 'دنبال کردن', + onCancel: (c) => context.pop(), + onConfirm: (c) async { + await launchUrl(Uri.parse('https://www.instagram.com/Houshan.ai'), + mode: LaunchMode.externalApplication) + .onError( + (error, stackTrace) { + if (kDebugMode) { + print('error open Link is: $error'); + } + return false; + }, + ); + }, + ); + } + + void showHeart({ + final Function()? onConfirm, + final Function()? onCancel, + }) async { + // Disabled by request: do nothing + return; + } + + void showEdit({ + final Function()? onConfirm, + final Function()? onCancel, + }) async { + await showAlertDialog( + title: '!هوشانی شو', + imageGif: Assets.icon.gif.write.image(), + description: + 'با تکمیل اطلاعات حساب کاربریت، علاوه بر اینکه 20 سکه هوشانی هدیه می‌گیری، به شکل بهینه‌تری از هوشان استفاده می‌کنی.', + onConfirmText: 'دعوت از دوستان', + onCancel: (c) => c.pop(), + onConfirm: (c) async {}, + ); + } + + void showGift({ + required final String mesasge, + final Function()? onConfirm, + final Function()? onCancel, + }) async { + await showAlertDialog( + title: 'یک هدیه ویژه شما', + imageGif: Assets.icon.gif.gift.image(), + description: mesasge, + ); + } + + void showWellcome({ + required final String mesasge, + final Function()? onConfirm, + final Function()? onCancel, + }) async { + await showAlertDialog( + imageGif: Assets.icon.gif.bell.image(), + description: mesasge, + ); + } + + void showCreateSuccess({ + final Function()? onConfirm, + final Function()? onCancel, + }) async { + await showAlertDialog( + onConfirm: (c) async => c.pop(), + onConfirmText: 'تایید', + imageGif: Assets.icon.gif.clock.image(), + description: + 'دستیار هوش مصنوعی شما با موفقیت ساخته شد.\nاین دستیار پس از تأیید در فهرست “دستیار‌های من” نمایش داده می‌شود.', + ); + } + + void showCoin({required final Function() onSuccess}) async { + await showAlertDialog( + title: 'تسویه حساب', + imageGif: Assets.icon.gif.coin.image(), + child: Padding( + padding: const EdgeInsets.only(top: 16), + child: BlocProvider( + create: (context) => SettlementCubit(), + child: BlocConsumer( + listener: (context, state) async { + if (state is SettlementSuccess) { + await context.read().getUserInfo(); + context.read().changeCredit(CreditModel( + credit: UserInfoCubit.userInfoModel.credit, + freeCredit: UserInfoCubit.userInfoModel.freeCredit)); + } + }, + builder: (context, state) { + String msg = 'خطا لحظاتی دیگر دوباره تلاش کنید'; + if (state is SettlementSuccess) { + msg = state.message; + onSuccess.call(); + } + if (state is SettlementFail) { + if (state.message != null) { + msg = state.message!; + } + } + return state is SettlementInitial + ? Column( + children: [ + Text( + 'شیوه دریافت درآمد خود را انتخاب کنید.', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface), + textDirection: TextDirection.rtl, + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: LoadingButton( + width: double.infinity, + color: AppColors.primaryColor.defaultShade, + onPressed: () async { + context.read().init(false); + }, + child: Text( + 'سکه', + style: AppTextStyles.body3 + .copyWith(color: Colors.white), + ), + )), + const SizedBox( + width: 8, + ), + Expanded( + child: LoadingButton( + width: double.infinity, + color: AppColors.primaryColor.defaultShade, + onPressed: + UserInfoCubit.userInfoModel.cardNumber == + null + ? null + : () { + context + .read() + .init(true); + }, + child: Text( + 'ریالی', + style: AppTextStyles.body3.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold), + ), + )), + ], + ), + if (UserInfoCubit.userInfoModel.cardNumber == null) + Padding( + padding: const EdgeInsets.all(4), + child: Directionality( + textDirection: TextDirection.rtl, + child: Row( + children: [ + Assets.icon.outline.warning2.svg( + color: AppColors.red.defaultShade, + width: 18, + height: 18), + const SizedBox( + width: 4, + ), + Flexible( + child: Text( + 'برای دریافت ریالی باید شماره کارت خود را در ویرایش پروفایل اضافه کنید', + textDirection: TextDirection.rtl, + style: AppTextStyles.body6.copyWith( + color: AppColors.red.defaultShade, + fontWeight: FontWeight.bold), + ), + ), + ], + ), + ), + ) + ], + ) + : state is SettlementLoading + ? SpinKitThreeBounce( + color: AppColors.primaryColor.defaultShade, + size: 32, + ) + : Text( + msg, + textDirection: TextDirection.rtl, + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: state is SettlementSuccess + ? AppColors.green.defaultShade + : AppColors.red.defaultShade), + ); + }, + ), + ), + )); + } + + void showExtras() async { + await showAlertDialog( + title: '', + imageGif: Assets.icon.gif.extras.image(), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + 'قبل از هر چیز لازم است بدانید که:', + style: AppTextStyles.body5.copyWith( + fontWeight: FontWeight.bold, + color: AppColors.gray[ + context.read().isDark() ? 600 : 900]), + textDirection: TextDirection.rtl, + ), + ], + ), + const SizedBox( + height: 8, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: Row( + textDirection: TextDirection.rtl, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Assets.icon.outline.verify.svg(), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + 'دسترسی شما تاریخ انقضاء ندارد', + style: AppTextStyles.body5.copyWith( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900]), + textDirection: TextDirection.rtl, + ), + ), + const SizedBox( + width: 8, + ), + Assets.icon.outline.tickSquare.svg(), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: Row( + textDirection: TextDirection.rtl, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Assets.icon.outline.verify.svg(), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + 'به همه مدل‌ها و ابزارها دسترسی دارید', + style: AppTextStyles.body5.copyWith( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900]), + textDirection: TextDirection.rtl, + ), + ), + const SizedBox( + width: 8, + ), + Assets.icon.outline.tickSquare.svg(), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: Row( + textDirection: TextDirection.rtl, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Assets.icon.outline.verify.svg(), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + 'پرداخت شما ارزان و بر اساس میزان استفاده شماست', + style: AppTextStyles.body5.copyWith( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900]), + textDirection: TextDirection.rtl, + ), + ), + const SizedBox( + width: 8, + ), + Assets.icon.outline.tickSquare.svg(), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: Row( + textDirection: TextDirection.rtl, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Assets.icon.outline.verify.svg(), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + 'بسته‌ی انتخابی شما بلافاصله پس از پرداخت فعال می‌شود.', + style: AppTextStyles.body5.copyWith( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900]), + textDirection: TextDirection.rtl, + ), + ), + const SizedBox( + width: 8, + ), + Assets.icon.outline.tickSquare.svg(), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: Row( + textDirection: TextDirection.rtl, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Assets.icon.outline.verify.svg(), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + 'حذف تبلیغات', + style: AppTextStyles.body5.copyWith( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900]), + textDirection: TextDirection.rtl, + ), + ), + const SizedBox( + width: 8, + ), + Assets.icon.outline.tickSquare.svg(), + ], + ), + ), + ], + )); + } + + Future showAlertDialog({ + final String? title, + required final Image imageGif, + final String? description, + final Widget? child, + final bool barrierDismissible = true, + final String onConfirmText = '', + final String onCancelText = 'بازگشت', + final Future Function(BuildContext contxet)? onConfirm, + final Function(BuildContext contxet)? onCancel, + }) async { + await showDialog( + context: context, + barrierDismissible: barrierDismissible, + builder: (context) { + final ValueNotifier loading = ValueNotifier(false); + return Responsive(context).maxWidthInDesktop( + maxWidth: maxDialogWidthInDesktop, + child: (contxet, maxWidth) => PopScope( + canPop: barrierDismissible, + child: Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (barrierDismissible) + Row( + children: [ + GestureDetector( + onTap: () => context.pop(), + child: Container( + width: 24, + height: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: + Theme.of(context).colorScheme.onSurface, + )), + child: Icon( + CupertinoIcons.xmark, + size: 18, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ) + ], + ), + Container( + width: 94, + height: 94, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppColors.secondryColor[50], + shape: BoxShape.circle), + child: imageGif, + ), + const SizedBox( + height: 8, + ), + if (title != null) + Text( + title, + style: AppTextStyles.body3.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface), + textDirection: TextDirection.rtl, + ), + if (description != null) + Column( + children: [ + const SizedBox( + height: 8, + ), + Text( + description, + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface), + textAlign: TextAlign.center, + textDirection: TextDirection.rtl, + ), + ], + ), + if (child != null) child, + const SizedBox( + height: 16, + ), + Row( + children: [ + if (onConfirm != null) + Expanded( + flex: 6, + child: ValueListenableBuilder( + valueListenable: loading, + builder: (context, l, _) { + return LoadingButton( + width: double.infinity, + loading: l, + color: + AppColors.primaryColor.defaultShade, + onPressed: () async { + loading.value = true; + await onConfirm.call(contxet); + loading.value = false; + }, + child: Text( + onConfirmText, + style: AppTextStyles.body5 + .copyWith(color: Colors.white), + ), + ); + })), + if (onConfirm != null && onCancel != null) + const SizedBox( + width: 24, + ), + if (onCancel != null) + Expanded( + flex: 4, + child: LoadingButton( + width: double.infinity, + color: Theme.of(context).colorScheme.secondary, + isOutlined: true, + backgroundColor: Theme.of(context) + .dialogTheme + .backgroundColor, + onPressed: () => onCancel.call(contxet), + child: Text( + onCancelText, + style: AppTextStyles.body5.copyWith( + color: Theme.of(context) + .colorScheme + .secondary, + fontWeight: FontWeight.bold), + ), + )), + ], + ) + ], + ), + ), + ), + ), + ); + }, + ); + } + + Future showPersonsAlert() async { + final TextEditingController textEditingController = TextEditingController(); + await showDialog( + context: context, + builder: (context) => Responsive(context).maxWidthInDesktop( + maxWidth: maxDialogWidthInDesktop, + child: (contxet, maxWidth) => Dialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Directionality( + textDirection: TextDirection.rtl, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text( + 'جای چه شخصیتی اینجا خالیه؟', + style: AppTextStyles.body3.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + ), + const SizedBox( + height: 16, + ), + AuthTextField( + controller: textEditingController, + hintText: 'دوست داری با چه کسی گپ بزنی؟', + label: 'نام شخصیت مد نظر', + ), + const SizedBox( + height: 32, + ), + Row( + children: [ + Expanded( + flex: 5, + child: LoadingButton( + onPressed: () { + contxet.pop(); + }, + width: double.infinity, + isOutlined: true, + backgroundColor: + Theme.of(context).dialogTheme.backgroundColor, + color: Theme.of(context).colorScheme.primary, + child: Text( + 'بازگشت', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold), + )), + ), + const SizedBox( + width: 12, + ), + Expanded( + flex: 6, + child: LoadingButton( + onPressed: () {}, + width: double.infinity, + child: Text( + 'تایید', + style: AppTextStyles.body4.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold), + )), + ), + ], + ) + ], + ), + ), + ), + ), + ), + ); + } + /* + Future showUpgradeCredit( + {final String? title, final Function()? onConfirm}) async { + final titleString = title == null ? '' : '" $title " '; + await showDialog( + context: context, + builder: (context) { + return Dialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: CircleIconBtn( + size: 48, + icon: Assets.icon.outline.crown, + color: AppColors.red[50], + iconColor: AppColors.red[100], + iconPadding: const EdgeInsets.all(12), + ), + ), + Text( + 'شما به هوش مصنوعی $titleStringدسترسی ندارید', + style: AppTextStyles.headline6, + textAlign: TextAlign.center, + ), + const SizedBox( + height: 4, + ), + Text( + 'برای دسترسی، اشتراک خود را ارتقا دهید ', + style: AppTextStyles.body4, + textAlign: TextAlign.center, + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + Flexible( + flex: 3, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: LoadingButton( + width: MediaQuery.sizeOf(context).width, + radius: 32, + color: AppColors.red.defaultShade, + child: Text('ارتقای اشتراک', + style: AppTextStyles.body4 + .copyWith(color: Colors.white)), + onPressed: () { + onConfirm?.call(); + + context.pop(); + }, + ), + )), + Flexible( + flex: 2, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: LoadingButton( + width: MediaQuery.sizeOf(context).width, + radius: 32, + isOutlined: true, + color: AppColors.red.defaultShade, + onPressed: () { + context.pop(); + }, + child: Text( + 'بازگشت', + style: AppTextStyles.body4 + .copyWith(color: AppColors.red.defaultShade), + ), + ), + )), + ], + ) + ], + ), + ), + ); + }, + ); + } + + Future showGiftCredit({final Function()? onConfirm}) async { + await showDialog( + context: context, + builder: (context) { + return Dialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + GestureDetector( + onTap: () => context.pop(), + child: Container( + width: 24, + height: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: AppColors.gray[900])), + child: const Icon( + CupertinoIcons.xmark, + size: 16, + ), + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: CircleIconBtn( + size: 86, + icon: Assets.icon.outline.coin, + color: AppColors.secondryColor[50], + iconColor: AppColors.secondryColor.defaultShade, + iconPadding: const EdgeInsets.all(12), + ), + ), + Text( + 'بسته اعتبار هدیه', + style: AppTextStyles.headline6, + textAlign: TextAlign.center, + ), + const SizedBox( + height: 4, + ), + Text( + '100 / 100', + style: + AppTextStyles.body4.copyWith(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32.0, vertical: 16), + child: LoadingButton( + radius: 32, + color: AppColors.primaryColor.defaultShade, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('مشاهده همه بسته‌ها', + style: AppTextStyles.body4 + .copyWith(color: Colors.white)), + const SizedBox( + width: 8, + ), + Assets.icon.outline.crown.svg(color: Colors.white), + ], + ), + onPressed: () { + onConfirm?.call(); + + context.pop(); + }, + ), + ), + ], + ), + ), + ); + }, + ); + } */ + + Future showPurchStatus( + {required final bool success, required final String detail}) async { + final color = + success ? AppColors.green.defaultShade : AppColors.red.defaultShade; + await showDialog( + context: context, + builder: (context) => Responsive(context).maxWidthInDesktop( + maxWidth: maxDialogWidthInDesktop, + child: (contxet, maxWidth) => Dialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 94, + height: 94, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: success ? AppColors.green[50] : AppColors.red[50], + shape: BoxShape.circle), + child: Icon( + success ? CupertinoIcons.check_mark : CupertinoIcons.xmark, + size: 80, + color: Colors.white, + ), + ), + const SizedBox( + height: 8, + ), + Text( + 'پرداخت ${success ? 'موفق' : 'ناموفق'}', + style: AppTextStyles.body3 + .copyWith(fontWeight: FontWeight.bold, color: color), + ), + Column( + children: [ + const SizedBox( + height: 8, + ), + Text( + detail, + style: AppTextStyles.body4, + textAlign: TextAlign.center, + textDirection: TextDirection.rtl, + ), + ], + ), + const SizedBox( + height: 16, + ), + ], + ), + ), + ), + ), + ); + } + + Future showCustomeReport() async { + await showDialog( + context: context, + builder: (context) => Responsive(context).maxWidthInDesktop( + maxWidth: maxDialogWidthInDesktop, + child: (contxet, maxWidth) => Dialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Directionality( + textDirection: TextDirection.rtl, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text( + 'گزارش اشکال', + style: AppTextStyles.body2.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold), + ), + ), + const SizedBox( + height: 12, + ), + const AuthTextField( + label: 'لطفاً توضیح دهید', + minLines: 6, + maxLines: 6, + ), + const SizedBox( + height: 32, + ), + Row( + children: [ + Expanded( + child: LoadingButton( + width: double.infinity, + onPressed: () { + context.pop(); + }, + child: Text( + 'بازگشت', + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Colors.white), + ))), + const SizedBox( + width: 24, + ), + Expanded( + child: LoadingButton( + width: double.infinity, + onPressed: () { + context.pop(); + context.pop(); + SnackBarManager(context, id: 'report-success') + .show( + status: SnackBarStatus.success, + message: 'گزارش با موفقیت ارسال شد'); + }, + isOutlined: true, + color: Theme.of(context).colorScheme.primary, + backgroundColor: + Theme.of(context).dialogTheme.backgroundColor, + child: Text( + 'گزارش', + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: + Theme.of(context).colorScheme.primary), + ))), + ], + ) + ], + ), + ), + ), + ), + ), + ); + } + + Future showShamsiYearMonthPicker( + {required Jalali initailDate, + required dynamic Function(Jalali selectedDate) onDateSelected}) async { + await showDialog( + context: context, + builder: (context) => Responsive(context).maxWidthInDesktop( + maxWidth: maxDialogWidthInDesktop, + child: (contxet, maxWidth) => Dialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: ShamsiYearMonthPicker( + onDateSelected: onDateSelected, + initailDate: initailDate, + ), + ), + ), + ), + ); + } + + Future showPrivateBots() async { + await showDialog( + context: context, + builder: (context) => Responsive(context).maxWidthInDesktop( + maxWidth: maxDialogWidthInDesktop, + child: (context, maxWidth) => Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.sizeOf(context).height * 0.5), + child: Dialog( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Directionality( + textDirection: TextDirection.rtl, + child: BlocBuilder( + builder: (context, state) { + if (state is BotsFail) { + return const SizedBox.shrink(); + } + if (state is BotsSuccess) { + final privateBots = state.privateBots; + if (privateBots.isEmpty) return const SizedBox.shrink(); + return Scrollbar( + thumbVisibility: true, + interactive: true, + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + children: [ + ListTile( + title: Text( + 'مدل‌های حرفه‌ای', + style: AppTextStyles.body3 + .copyWith(fontWeight: FontWeight.bold), + ), + leading: Assets.icon.outline.crown.svg( + color: Theme.of(context) + .colorScheme + .onSurface), + ), + ListView.builder( + itemCount: privateBots.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric( + horizontal: 16), + itemBuilder: (context, index) { + final bot = privateBots[index]; + return InkWell( + onTap: () { + context.push(Routes.chat, + extra: ChatArgs(bot: bot)); + }, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 8), + decoration: BoxDecoration( + border: index != + privateBots.length - 1 + ? Border( + bottom: BorderSide( + color: AppColors + .gray[context + .read< + ThemeModeCubit>() + .isDark() + ? 600 + : 900])) + : null), + child: Row( + children: [ + ImageNetwork( + url: bot.image, + width: 32, + height: 32, + radius: 360, + color: bot.image != null && + bot.image! + .contains('/llm') + ? Theme.of(context) + .colorScheme + .onSurface + : null, + fit: BoxFit.cover, + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + bot.name ?? '', + maxLines: 1, + overflow: + TextOverflow.ellipsis, + style: AppTextStyles.body4 + .copyWith( + color: + Theme.of(context) + .colorScheme + .onSurface, + fontWeight: + FontWeight.bold), + ), + if (bot.description != null) + Text(bot.description!, + style: AppTextStyles.body5 + .copyWith( + color: AppColors + .gray[context + .read< + ThemeModeCubit>() + .isDark() + ? 600 + : 900])) + ], + )), + const SizedBox( + width: 8, + ), + Container( + padding: + const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .primary, + borderRadius: + BorderRadius.circular( + 16)), + child: Center( + child: Text( + bot.cost == 0 || + bot.cost == null + ? 'رایگان' + : '${bot.cost} سکه', + style: AppTextStyles.body6 + .copyWith( + color: Colors.white), + ), + ), + ) + ], + ), + ), + ); + }, + ), + ], + ), + ), + ); + } + + return Column( + children: [ + DefaultPlaceHolder( + child: ListTile( + title: Text( + 'مدل‌های هوش مصنوعی حرفه‌ای', + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context) + .colorScheme + .onSurface), + ), + leading: Assets.icon.outline.crown.svg( + color: + Theme.of(context).colorScheme.onSurface), + ), + ), + ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: 10, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, index) { + return DefaultPlaceHolder( + child: Container( + height: 32, + margin: const EdgeInsets.symmetric(vertical: 8), + decoration: + const BoxDecoration(color: Colors.white), + )); + }, + ), + const SizedBox( + height: 16, + ) + ], + ); + }, + ), + ), + ), + ), + ), + ), + ), + ); + } + + void onMusicCreate({ + final Function()? onConfirm, + final Function()? onCancel, + }) async { + // Disabled by request: do nothing + return; + } + + void onVideoCreate({ + final Function()? onConfirm, + final Function()? onCancel, + }) async { + // Disabled by request: do nothing + return; + } + + void onPhotoCreated({ + final Function()? onConfirm, + final Function()? onCancel, + }) async { + // Disabled by request: do nothing + return; + } + + void conditionsForCmp({required final String awards}) async { + await showDialog( + context: context, + builder: (context) => Responsive(context).maxWidthInDesktop( + maxWidth: maxDialogWidthInDesktop, + child: (contxet, maxWidth) => Dialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Directionality( + textDirection: TextDirection.rtl, + child: ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + '📑 شرایط شرکت در مسابقه', + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + ), + ), + const SizedBox( + height: 16, + ), + Scrollbar( + thumbVisibility: true, + trackVisibility: true, + interactive: true, + child: Directionality( + textDirection: TextDirection.rtl, + child: Container( + margin: const EdgeInsets.only(right: 8), + constraints: BoxConstraints( + maxHeight: MediaQuery.sizeOf(context).height * 0.4), + child: SingleChildScrollView( + physics: BouncingScrollPhysics(), + child: Html( + data: awards, + shrinkWrap: true, + onLinkTap: (url, attributes, element) async { + try { + await launchUrl(Uri.parse(url!), + mode: LaunchMode.externalApplication) + .onError( + (error, stackTrace) { + if (kDebugMode) { + print('error open Link is: $error'); + } + return false; + }, + ); + } catch (e) { + if (kDebugMode) { + print(e); + } + } + }, + style: { + 'p': Style( + fontFamily: AppTextStyles.defaultFontFamily, + color: + Theme.of(context).colorScheme.onSurface, + fontSize: FontSize(16)) + }), + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + LoadingButton( + width: double.infinity, + onPressed: () { + contxet.pop(); + }, + color: Theme.of(context).colorScheme.primary, + child: Text( + 'بازگشت', + style: AppTextStyles.body4.copyWith(color: Colors.white), + )) + ], + ), + ), + ), + ), + ); + } + + void rewardForCmp({required final String rewards}) async { + await showDialog( + context: context, + builder: (context) => Responsive(context).maxWidthInDesktop( + maxWidth: maxDialogWidthInDesktop, + child: (contxet, maxWidth) => Dialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Directionality( + textDirection: TextDirection.rtl, + child: ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + '🏆 جوایز مسابقه', + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + ), + ), + const SizedBox( + height: 16, + ), + Scrollbar( + thumbVisibility: true, + trackVisibility: true, + interactive: true, + child: Directionality( + textDirection: TextDirection.rtl, + child: Container( + margin: const EdgeInsets.only(right: 8), + constraints: BoxConstraints( + maxHeight: MediaQuery.sizeOf(context).height * 0.4), + child: SingleChildScrollView( + physics: BouncingScrollPhysics(), + child: Html( + data: rewards, + shrinkWrap: true, + onLinkTap: (url, attributes, element) async { + try { + await launchUrl(Uri.parse(url!), + mode: LaunchMode.externalApplication) + .onError( + (error, stackTrace) { + if (kDebugMode) { + print('error open Link is: $error'); + } + return false; + }, + ); + } catch (e) { + if (kDebugMode) { + print(e); + } + } + }, + style: { + 'p': Style( + fontFamily: AppTextStyles.defaultFontFamily, + color: + Theme.of(context).colorScheme.onSurface, + fontSize: FontSize(16)), + }), + ), + ), + )), + const SizedBox( + height: 16, + ), + LoadingButton( + width: double.infinity, + onPressed: () { + contxet.pop(); + }, + color: Theme.of(context).colorScheme.primary, + child: Text( + 'بازگشت', + style: AppTextStyles.body4.copyWith(color: Colors.white), + )) + ], + ), + ), + ), + ), + ); + } + + void winnersForCmp({required final List winners}) async { + await showDialog( + context: context, + builder: (context) => Responsive(context).maxWidthInDesktop( + maxWidth: maxDialogWidthInDesktop, + child: (contxet, maxWidth) => Dialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Directionality( + textDirection: TextDirection.rtl, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + '✌️ نفرات برتر مسابقه', + style: AppTextStyles.headline6.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + ), + const SizedBox( + height: 16, + ), + ListView.builder( + itemCount: winners.length, + shrinkWrap: true, + itemBuilder: (context, index) { + final winner = winners[index]; + return ListTile( + leading: const CircleAvatar(), + title: Text('${winner.rank ?? 1}', + style: AppTextStyles.body3.copyWith( + color: Theme.of(context).colorScheme.onSurface, + )), + subtitle: Text.rich( + TextSpan(text: winner.username ?? '', children: [ + TextSpan( + text: 'مشاهده اثر', + style: TextStyle( + color: + Theme.of(context).colorScheme.primary)) + ]), + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + ); + }, + ), + const SizedBox( + height: 16, + ), + LoadingButton( + width: double.infinity, + onPressed: () { + contxet.pop(); + }, + color: Theme.of(context).colorScheme.primary, + child: Text( + 'بازگشت', + style: + AppTextStyles.body4.copyWith(color: Colors.white), + )) + ], + ), + ), + ), + ), + ), + ); + } + + Future updateAlert( + {required final String message, + required final bool force, + required final String version}) async { + final versionStyle = AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface); + await showAlertDialog( + barrierDismissible: !force, + imageGif: Assets.icon.gif.waveHand.image(), + title: 'به روزرسانی هوشان', + onConfirmText: 'به روزرسانی', + onCancelText: force ? '' : '!الان نه', + onCancel: force ? null : (c) => context.pop(), + onConfirm: (c) async { + await launchUrl(Uri.parse('https://api.houshan.ai/apk'), + mode: LaunchMode.externalApplication) + .onError( + (error, stackTrace) { + if (kDebugMode) { + print('error open Link is: $error'); + } + return false; + }, + ); + }, + child: Column( + children: [ + SizedBox( + height: 8, + ), + Text(message, + textDirection: TextDirection.rtl, + textAlign: TextAlign.justify, + style: AppTextStyles.body4 + .copyWith(color: Theme.of(context).colorScheme.onSurface)), + SizedBox( + height: 16, + ), + Row( + children: [ + Expanded( + child: Center( + child: FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: (context, snapshot) { + String version = '...'; + if (snapshot.hasData && snapshot.data != null) { + version = snapshot.data!.version; + } + return Text(version, style: versionStyle); + }), + )), + Text('>>>', style: versionStyle), + Expanded( + child: Center( + child: Text(version, style: versionStyle), + )), + ], + ) + ], + )); + } +} diff --git a/lib/ui/widgets/components/dropdown/animated_setting_container.dart b/lib/ui/widgets/components/dropdown/animated_setting_container.dart new file mode 100644 index 0000000..23ccfbf --- /dev/null +++ b/lib/ui/widgets/components/dropdown/animated_setting_container.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/animations/animated_visibility.dart'; + +class AnimatedSettingContainer extends StatefulWidget { + final String title; + final IconData icon; + final List childrens; + const AnimatedSettingContainer( + {super.key, + required this.title, + required this.icon, + required this.childrens}); + + @override + State createState() => + _AnimatedSettingContainerState(); +} + +class _AnimatedSettingContainerState extends State { + final ValueNotifier show = ValueNotifier(false); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + show.value = !show.value; + }, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border.all(color: AppColors.gray.defaultShade), + borderRadius: BorderRadius.circular(18)), + child: ValueListenableBuilder( + valueListenable: show, + builder: (context, isVisible, child) => Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Icon( + isVisible ? Icons.minimize : Icons.add, + size: 18, + color: AppColors.secondryColor.defaultShade, + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + widget.title, + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface), + textDirection: TextDirection.rtl, + ), + ), + ], + ), + AnimatedVisibility( + isVisible: isVisible, + duration: const Duration(milliseconds: 300), + child: Column( + children: widget.childrens, + )) + ], + ), + ), + ), + ); + } +} diff --git a/lib/ui/widgets/components/dropdown/bots_search_dropdown.dart b/lib/ui/widgets/components/dropdown/bots_search_dropdown.dart new file mode 100644 index 0000000..b802146 --- /dev/null +++ b/lib/ui/widgets/components/dropdown/bots_search_dropdown.dart @@ -0,0 +1,175 @@ +import 'package:animated_custom_dropdown/custom_dropdown.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; + +import 'package:hoshan/data/model/ai/bots_model.dart'; +import 'package:hoshan/ui/screens/main/home/bloc/bots_bloc.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; + +class BotSearchDropdown extends StatefulWidget { + final Function(Bots? bot)? onSelectedBot; + const BotSearchDropdown({super.key, this.onSelectedBot}); + + @override + State createState() => _BotSearchDropdownState(); +} + +class _BotSearchDropdownState extends State { + late final CustomDropdownDecoration customDropdownDecoration = + CustomDropdownDecoration( + expandedFillColor: const Color(0xffE0ECFF), + closedFillColor: const Color(0xffE0ECFF), + closedSuffixIcon: Icon( + Icons.arrow_drop_down_rounded, + color: AppColors.primaryColor.defaultShade, + ), + expandedSuffixIcon: Icon( + Icons.arrow_drop_up_rounded, + color: AppColors.primaryColor.defaultShade, + ), + overlayScrollbarDecoration: Theme.of(context).scrollbarTheme); + + final List _list = []; + Bots? initailBot; + + Future> _getFakeRequestData(String query) async { + return await Future.delayed(const Duration(seconds: 1), () { + return _list.where((e) { + return e.name.toString().toLowerCase().contains(query.toLowerCase()); + }).toList(); + }); + } + + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.rtl, + child: BlocConsumer( + listener: (context, state) {}, + builder: (context, state) { + if (state is BotsSuccess) { + _list.clear(); + _list.addAll(BotsBloc.allBots); + // if (_list.isNotEmpty) { + // HomeCubit.bot.value = _list.first; + // initailBot = _list.first; + // } + } + if (state is BotsFail) { + return const SizedBox(); + } + return DefaultPlaceHolder( + enabled: state is BotsLoading, + child: CustomDropdown.searchRequest( + items: _list, + futureRequest: _getFakeRequestData, + searchHintText: 'جستجو در بات ها', + overlayHeight: MediaQuery.sizeOf(context).height / 3, + canCloseOutsideBounds: false, + excludeSelected: true, + headerBuilder: botView, + hintBuilder: hintView, + listItemPadding: EdgeInsets.zero, + listItemBuilder: listItemView, + decoration: customDropdownDecoration, + // initialItem: initailBot, + onChanged: (value) { + widget.onSelectedBot?.call(value); + }, + ), + ); + }, + ), + ); + } + + Widget listItemView(BuildContext context, Bots item, bool isSelected, + void Function() onItemSelect) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + decoration: BoxDecoration( + border: Border( + // top: BorderSide(width: 1, color: Colors.black), + bottom: BorderSide( + width: 1, + color: _list.last != item + ? AppColors.gray.defaultShade + : const Color(0xffE0ECFF)))), + child: Center(child: botView(context, item, true))); + } + + Widget hintView(BuildContext context, String hint, bool enabled) { + return Row( + children: [ + Assets.icon.outline.brain.svg(width: 18, height: 18), + const SizedBox( + width: 12, + ), + Expanded( + child: Center( + child: Text( + 'انتخاب نوع هوش مصنوعی', + style: AppTextStyles.body4.copyWith(fontWeight: FontWeight.bold), + ), + ), + ), + ], + ); + } + + Row botView(BuildContext context, Bots item, bool enabled) { + return Row( + children: [ + ClipOval( + child: SizedBox( + width: 24, + height: 24, + child: CachedNetworkImage( + imageUrl: item.image!, + fit: BoxFit.cover, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.name!, + style: + AppTextStyles.body3.copyWith(fontWeight: FontWeight.bold), + ), + // Text( + // item.description, + // style: AppTextStyles.body5.copyWith( + // color: AppColors.gray[800], + // overflow: TextOverflow.ellipsis), + // maxLines: 1, + // ) + ], + ), + ), + ), + // if (item.lock) + // Container( + // width: 24, + // height: 24, + // decoration: BoxDecoration( + // shape: BoxShape.circle, + // color: AppColors.primaryColor.defaultShade), + // child: const Icon( + // Icons.lock, + // size: 12, + // color: Colors.white, + // ), + // ) + ], + ); + } +} diff --git a/lib/ui/widgets/components/dropdown/hint_tooltip.dart b/lib/ui/widgets/components/dropdown/hint_tooltip.dart new file mode 100644 index 0000000..1c6ae34 --- /dev/null +++ b/lib/ui/widgets/components/dropdown/hint_tooltip.dart @@ -0,0 +1,48 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter/material.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/ui/theme/text.dart'; + +class HintTooltip extends StatelessWidget { + final String hint; + final double? size; + final Widget? child; + final Color? iconColor; + const HintTooltip( + {super.key, required this.hint, this.child, this.size, this.iconColor}); + + @override + Widget build(BuildContext context) { + final color = iconColor ?? Theme.of(context).colorScheme.primary; + return Tooltip( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8)), + triggerMode: TooltipTriggerMode.tap, + enableTapToDismiss: true, + enableFeedback: true, + preferBelow: true, + showDuration: const Duration(minutes: 2), + richMessage: WidgetSpan( + alignment: PlaceholderAlignment.baseline, + baseline: TextBaseline.alphabetic, + child: Container( + padding: const EdgeInsets.all(10), + constraints: const BoxConstraints(maxWidth: 300), + child: Text( + hint, + style: AppTextStyles.body4 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + textDirection: TextDirection.rtl, + textAlign: TextAlign.justify, + ), + ), + ), + child: child ?? + Assets.icon.outline.warning2 + .svg(width: size, height: size, color: color), + ); + } +} diff --git a/lib/ui/widgets/components/dropdown/more_popup_menu.dart b/lib/ui/widgets/components/dropdown/more_popup_menu.dart new file mode 100644 index 0000000..aec90a8 --- /dev/null +++ b/lib/ui/widgets/components/dropdown/more_popup_menu.dart @@ -0,0 +1,104 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter/material.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/ui/theme/text.dart'; + +class MorePopupMenuHandler { + final BuildContext context; + + MorePopupMenuHandler({required this.context}); + + static Widget morePopUpItem( + {final SvgGenImage? icon, + final Icon? customeIcon, + required final String title, + Color? color}) { + return Directionality( + textDirection: TextDirection.rtl, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Row( + children: [ + if (icon != null) icon.svg(color: color, width: 18, height: 18), + if (customeIcon != null) customeIcon, + const SizedBox( + width: 8, + ), + Text( + title, + style: AppTextStyles.body4.copyWith(color: color), + ), + ], + ), + ), + ); + } + + Widget morePopUpMenu( + {required final Widget child, + final Color? color, + final List items = const []}) { + return PopupMenuButton( + tooltip: '', + offset: const Offset(0, 38), + onSelected: (value) async { + items[value].click?.call(); + }, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + itemBuilder: (BuildContext context) { + return >[ + ...List.generate( + items.length, (index) => items[index].popupMenuItem) + ]; + }, + child: child); + } + + void showMorePopupMenu( + {required final GlobalKey containerKey, + final Color? color, + final bool right = false, + final List items = const []}) async { + final RenderBox overlay = + Overlay.of(context).context.findRenderObject() as RenderBox; + final RenderBox containerRenderBox = + containerKey.currentContext!.findRenderObject() as RenderBox; + + final Offset containerPosition = + containerRenderBox.localToGlobal(Offset.zero); + final Size containerSize = containerRenderBox.size; + + await showMenu( + context: context, + shape: ContinuousRectangleBorder(borderRadius: BorderRadius.circular(24)), + color: color, + position: RelativeRect.fromLTRB( + containerPosition.dx - (right ? 32 : 0), + (containerPosition.dy + containerSize.height + 8), + (overlay.size.width - containerPosition.dx) - (right ? 0 : 120), + overlay.size.height - containerPosition.dy - containerSize.height), + items: [ + ...items.map( + (e) => e.popupMenuItem, + ) + ], + ).then((value) async { + if (value != null) { + items + .firstWhere( + (element) => element.popupMenuItem.value == value, + ) + .click + ?.call(); + } + }); + } +} + +class PopUpMenuItemModel { + final PopupMenuItem popupMenuItem; + final Function()? click; + + PopUpMenuItemModel({required this.popupMenuItem, this.click}); +} diff --git a/lib/ui/widgets/components/dropdown/multi_select_dropdown.dart b/lib/ui/widgets/components/dropdown/multi_select_dropdown.dart new file mode 100644 index 0000000..6e6232e --- /dev/null +++ b/lib/ui/widgets/components/dropdown/multi_select_dropdown.dart @@ -0,0 +1,35 @@ +import 'package:animated_custom_dropdown/custom_dropdown.dart'; +import 'package:flutter/material.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; + +class MultiSelectDropdown extends StatelessWidget { + final String hintText; + final List items; + final Function(List)? onListChanged; + final Widget Function(BuildContext context, dynamic item, bool isSelected, + Function() onItemSelect)? child; + const MultiSelectDropdown( + {super.key, + required this.hintText, + this.onListChanged, + this.child, + required this.items}); + + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.rtl, + child: CustomDropdown.multiSelect( + items: items, + // initialItems: ['item1'], + hintText: hintText, + listItemBuilder: child, + decoration: CustomDropdownDecoration( + hintStyle: AppTextStyles.body4.copyWith(color: AppColors.gray[700]), + closedFillColor: Theme.of(context).scaffoldBackgroundColor), + onListChanged: onListChanged, + ), + ); + } +} diff --git a/lib/ui/widgets/components/dropdown/simple_dropdown.dart b/lib/ui/widgets/components/dropdown/simple_dropdown.dart new file mode 100644 index 0000000..2c79b02 --- /dev/null +++ b/lib/ui/widgets/components/dropdown/simple_dropdown.dart @@ -0,0 +1,56 @@ +import 'package:animated_custom_dropdown/custom_dropdown.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/text.dart'; + +class SimpleDropdown extends StatelessWidget { + final String? hintText; + final String? initialItem; + final List list; + final Function(int)? onSelect; + final double? width; + final double? height; + const SimpleDropdown({ + super.key, + this.hintText, + required this.list, + this.onSelect, + this.width, + this.height, + this.initialItem, + }); + + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.rtl, + child: SizedBox( + width: width, + height: height, + child: CustomDropdown( + decoration: CustomDropdownDecoration( + expandedFillColor: context.read().isDark() + ? Theme.of(context).scaffoldBackgroundColor + : AppColors.primaryColor[50], + closedFillColor: context.read().isDark() + ? Theme.of(context).colorScheme.onSurface.withAlpha(80) + : AppColors.primaryColor[50], + headerStyle: AppTextStyles.body4 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + listItemStyle: AppTextStyles.body4 + .copyWith(color: Theme.of(context).colorScheme.onSurface)), + hintText: hintText, + items: list, + initialItem: initialItem ?? list[0], + onChanged: (p0) { + final index = list.indexOf(p0 ?? ''); + + onSelect?.call(index); + }, + ), + ), + ); + } +} diff --git a/lib/ui/widgets/components/image/custome_banner.dart b/lib/ui/widgets/components/image/custome_banner.dart new file mode 100644 index 0000000..ee1d693 --- /dev/null +++ b/lib/ui/widgets/components/image/custome_banner.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:hoshan/data/model/banner_model.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; + +class CustomeBanner extends StatefulWidget { + final BannerModel bannerModel; + final double width; + final double height; + const CustomeBanner( + this.bannerModel, { + super.key, + required this.width, + required this.height, + }); + + @override + State createState() => _CustomeBannerState(); +} + +class _CustomeBannerState extends State { + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(16), + child: SizedBox( + width: widget.width, + height: widget.height, + child: Stack( + children: [ + Row( + children: [ + const Expanded(child: SizedBox.shrink()), + Expanded( + flex: 2, + child: ImageNetwork( + height: widget.height, + url: widget.bannerModel.imageUrl, + error: AspectRatio( + aspectRatio: 1 / 1, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + ...widget.bannerModel.colors, + ])), + ), + ), + placeholder: AspectRatio( + aspectRatio: 1 / 1, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + ...widget.bannerModel.colors, + ])), + ), + ), + )), + ], + ), + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient(colors: [ + Colors.transparent, + ...widget.bannerModel.colors + ], begin: Alignment.bottomRight, end: Alignment.topLeft)), + ), + ), + Positioned.fill(child: widget.bannerModel.child), + ], + ), + ), + ); + } +} diff --git a/lib/ui/widgets/components/image/custome_image.dart b/lib/ui/widgets/components/image/custome_image.dart new file mode 100644 index 0000000..4a2e136 --- /dev/null +++ b/lib/ui/widgets/components/image/custome_image.dart @@ -0,0 +1,25 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class CustomeImage extends StatelessWidget { + final String src; + final BoxFit? fit; + const CustomeImage({super.key, required this.src, this.fit}); + + @override + Widget build(BuildContext context) { + return kIsWeb + ? Image.network( + src, + fit: fit, + ) + : Image.file( + File( + src, + ), + fit: fit, + ); + } +} diff --git a/lib/ui/widgets/components/image/network_image.dart b/lib/ui/widgets/components/image/network_image.dart new file mode 100644 index 0000000..f3b1d02 --- /dev/null +++ b/lib/ui/widgets/components/image/network_image.dart @@ -0,0 +1,166 @@ +// ignore_for_file: curly_braces_in_flow_control_structures + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/data/storage/shared_preferences_helper.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/widgets/components/dialog/dialog_handler.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; +import 'package:string_validator/string_validator.dart'; +import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart'; + +class ImageNetwork extends StatefulWidget { + final String? url; + final String? baseUrl; + final double radius; + final double? width; + final double? height; + final bool showHero; + final Widget? error; + final Widget? placeholder; + final BoxFit fit; + final Color? color; + final BlendMode? blendMode; + const ImageNetwork( + {super.key, + required this.url, + this.radius = 0, + this.showHero = false, + this.error, + this.width, + this.height, + this.fit = BoxFit.cover, + this.baseUrl, + this.placeholder, + this.color, + this.blendMode}); + + @override + State createState() => _ImageNetworkState(); +} + +class _ImageNetworkState extends State { + @override + Widget build(BuildContext context) { + bool inError = false; + final token = AuthTokenStorage.getToken(); + + if (widget.url == null) + return SizedBox( + width: widget.width, + height: widget.height, + child: ClipRRect( + borderRadius: BorderRadius.circular(widget.radius), + child: Container( + decoration: + BoxDecoration(color: Theme.of(context).colorScheme.surface), + child: (context.read().isDark() + ? Assets.icon.launcherIcons.houshanIconWhie + : Assets.icon.launcherIcons.houshanIconPrimary) + .image(), + ), + ), + ); + final url = + '${widget.baseUrl ?? (widget.url!.isURL() ? '' : DioService.baseURL)}${widget.url}'; + if (widget.url!.isEmpty) return const SizedBox.shrink(); + return GestureDetector( + onTap: widget.showHero + ? () { + if (!inError) { + DialogHandler(context: context) + .showImageHero(image: widget.url!); + } + } + : null, + child: SizedBox( + width: widget.width, + height: widget.height, + child: ClipRRect( + borderRadius: BorderRadius.circular(widget.radius), + child: widget.url!.split('.').last == 'svg' + ? SvgPicture.network( + widget.url!, + fit: widget.fit, + headers: url.startsWith(DioService.baseURL) + ? { + 'Authorization': "Bearer $token", + } + : null, + placeholderBuilder: (context) => + widget.placeholder ?? + DefaultPlaceHolder( + child: Container( + height: MediaQuery.sizeOf(context).height, + width: MediaQuery.sizeOf(context).width, + color: Colors.white, + ), + ), + errorBuilder: (context, url, error) { + inError = true; + if (kDebugMode) { + print("Catch image with Url: $url Failed Error: $error"); + } + + return widget.error ?? + AspectRatio( + aspectRatio: 1 / 1, + child: Container( + color: Theme.of(context).colorScheme.surface, + child: Icon( + Icons.image_not_supported_outlined, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ); + }, + ) + : CachedNetworkImage( + fit: widget.fit, + color: widget.color, + colorBlendMode: widget.blendMode, + httpHeaders: url.startsWith(DioService.baseURL) + ? { + 'Authorization': "Bearer $token", + } + : null, + imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, + imageUrl: url, + placeholder: (context, url) => + widget.placeholder ?? + DefaultPlaceHolder( + child: Container( + height: MediaQuery.sizeOf(context).height, + width: MediaQuery.sizeOf(context).width, + color: Colors.white, + ), + ), + errorWidget: (context, url, error) { + inError = true; + if (kDebugMode) { + print("Catch image with Url: $url Failed Error: $error"); + } + + return widget.error ?? + AspectRatio( + aspectRatio: 1 / 1, + child: Container( + color: Theme.of(context).colorScheme.surface, + child: Icon( + Icons.image_not_supported_outlined, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/ui/widgets/components/purchase/cubit/discount_cubit.dart b/lib/ui/widgets/components/purchase/cubit/discount_cubit.dart new file mode 100644 index 0000000..66d6c9c --- /dev/null +++ b/lib/ui/widgets/components/purchase/cubit/discount_cubit.dart @@ -0,0 +1,33 @@ +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hoshan/data/model/discount_model.dart'; +import 'package:hoshan/data/repository/paymant_repository.dart'; + +part 'discount_state.dart'; + +class DiscountCubit extends Cubit { + DiscountCubit() : super(DiscountInitial()); + + void verifyDiscount(String code, String id) async { + emit(DiscountLoading()); + try { + final response = await PaymantRepository.getDiscount(code, id); + emit(DiscountSuccess(discount: response, code: code)); + } on DioException catch (e) { + try { + emit(DiscountFail(message: e.response?.data['detail'])); + } catch (e) { + emit(const DiscountFail()); + } + if (kDebugMode) { + print('Dio Error is: $e'); + } + } + } + + Future refresh() async { + emit(DiscountInitial()); + } +} diff --git a/lib/ui/widgets/components/purchase/cubit/discount_state.dart b/lib/ui/widgets/components/purchase/cubit/discount_state.dart new file mode 100644 index 0000000..0fcfaae --- /dev/null +++ b/lib/ui/widgets/components/purchase/cubit/discount_state.dart @@ -0,0 +1,25 @@ +part of 'discount_cubit.dart'; + +sealed class DiscountState extends Equatable { + const DiscountState(); + + @override + List get props => []; +} + +final class DiscountInitial extends DiscountState {} + +final class DiscountLoading extends DiscountState {} + +final class DiscountSuccess extends DiscountState { + final DiscountModel discount; + final String code; + + const DiscountSuccess({required this.discount, required this.code}); +} + +final class DiscountFail extends DiscountState { + final String? message; + + const DiscountFail({this.message}); +} diff --git a/lib/ui/widgets/components/purchase/purchase_card.dart b/lib/ui/widgets/components/purchase/purchase_card.dart new file mode 100644 index 0000000..65c5253 --- /dev/null +++ b/lib/ui/widgets/components/purchase/purchase_card.dart @@ -0,0 +1,457 @@ +// ignore_for_file: use_build_context_synchronously, deprecated_member_use_from_same_package + +import 'dart:math'; + +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/data/model/plans_model.dart'; +import 'package:hoshan/data/repository/paymant_repository.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/components/dialog/dialog_handler.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:hoshan/ui/widgets/components/purchase/cubit/discount_cubit.dart'; +import 'package:hoshan/ui/widgets/components/shapes/vertical_ribbon.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; +import 'package:persian_number_utility/persian_number_utility.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class PurchaseCard extends StatefulWidget { + final Plans plan; + final String? label; + final double? height; + final Widget Function()? button; + const PurchaseCard({ + super.key, + required this.plan, + this.label, + this.height, + this.button, + }); + + @override + State createState() => _PurchaseCardState(); +} + +class _PurchaseCardState extends State { + ValueNotifier loading = ValueNotifier(false); + ValueNotifier showDiscount = ValueNotifier(false); + final TextEditingController discountTextEditingController = + TextEditingController(); + String? discountCode; + late final pr = widget.plan; + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => DiscountCubit(), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Stack( + children: [ + Container( + height: widget.height, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Theme.of(context).colorScheme.surface), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: ImageNetwork( + url: pr.image, + radius: 360, + width: 94, + height: 94, + ), + ), + const SizedBox( + height: 8, + ), + Text( + widget.plan.title ?? '', + style: AppTextStyles.headline5.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + if (widget.plan.desc != null) + Column( + children: [ + const SizedBox( + height: 4, + ), + Text( + widget.plan.desc!, + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900]), + ), + const SizedBox( + height: 16, + ), + ], + ), + const SizedBox( + height: 8, + ), + if (pr.price != null) + Column( + children: [ + Text( + '${pr.coins} سکه هوشان + ${pr.freeCoins} سکه رایگان', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface), + textAlign: TextAlign.center, + textDirection: TextDirection.rtl, + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + textDirection: TextDirection.rtl, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text(':مبلغ قابل پرداخت', + style: AppTextStyles.body3.copyWith( + color: Theme.of(context) + .colorScheme + .primary)), + ], + ), + Row( + children: [ + if (pr.oldPrice != null) + Stack( + children: [ + Text( + '${'${pr.oldPrice}'.seRagham()} تومان', + style: AppTextStyles.body4.copyWith( + color: AppColors + .gray.defaultShade)), + Positioned.fill( + child: Divider( + color: AppColors.gray.defaultShade, + thickness: 4, + )) + ], + ), + if (pr.oldPrice != null) + Text(' - ', + style: AppTextStyles.body4.copyWith( + color: + AppColors.green.defaultShade)), + Text('${'${pr.price}'.seRagham()} تومان', + style: AppTextStyles.body4.copyWith( + color: AppColors.green.defaultShade)), + ], + ) + ], + ), + ), + const SizedBox( + height: 32, + ), + ValueListenableBuilder( + valueListenable: showDiscount, + builder: (context, show, _) { + return show + ? BlocConsumer( + listener: (context, state) { + if (state is DiscountSuccess) { + final p = pr.price! - + (state.discount.percent != null + ? min( + (pr.price! * + state.discount + .percent!) ~/ + 100, + state.discount + .maxValue!) + : state.discount.value!) + .toInt(); + + pr.oldPrice = pr.price; + pr.price = p.round(); + + setState(() { + discountCode = state.code; + }); + } + }, + builder: (context, state) { + return Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + Expanded( + flex: 1, + child: Row( + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + LoadingButton( + loading: state + is DiscountLoading, + onPressed: () { + if (discountTextEditingController + .text.isEmpty) { + return; + } + if (state + is DiscountSuccess) { + pr.price = + pr.oldPrice; + pr.oldPrice = null; + + setState(() { + discountCode = null; + }); + context + .read< + DiscountCubit>() + .refresh(); + + return; + } + context + .read< + DiscountCubit>() + .verifyDiscount( + discountTextEditingController + .text, + pr.id ?? ''); + }, + color: state + is DiscountSuccess + ? AppColors + .red.defaultShade + : AppColors.green + .defaultShade, + child: Text( + state is DiscountSuccess + ? 'حذف' + : 'اعمال', + style: AppTextStyles + .body4 + .copyWith( + color: Colors + .white, + fontWeight: + FontWeight + .bold), + )) + ], + ), + ), + Expanded( + flex: 2, + child: Directionality( + textDirection: + TextDirection.rtl, + child: TextField( + controller: + discountTextEditingController, + maxLength: 8, + maxLines: 1, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSurface), + enabled: state + is! DiscountSuccess && + state is! DiscountLoading, + buildCounter: (context, + {required currentLength, + required isFocused, + required maxLength}) => + const SizedBox.shrink(), + onChanged: (value) { + if (state + is! DiscountInitial) { + context + .read() + .refresh(); + } + }, + decoration: InputDecoration( + error: + state is DiscountFail + ? Text( + state.message ?? + 'مشکلی پیش آمده لحظاتی دیگر دوباره امتحان کنید', + style: AppTextStyles + .body5 + .copyWith( + color: AppColors + .red + .defaultShade), + ) + : null, + suffixIcon: + GestureDetector( + onTap: () async { + ClipboardData? + clipboardData = + await Clipboard + .getData( + 'text/plain'); + if (clipboardData != + null && + clipboardData + .text != + null) { + discountTextEditingController + .text = + clipboardData + .text!; + } + }, + child: const Icon( + Icons + .paste_rounded)), + hintText: + 'کد تخفیف دارید؟', + hintStyle: + AppTextStyles.body4), + ), + ), + ) + ], + ); + }, + ) + : GestureDetector( + onTap: () => showDiscount.value = !show, + child: Text( + 'کد تخفیف دارید؟ اینجا کلیک کنید.', + textDirection: TextDirection.rtl, + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context) + .colorScheme + .primary), + ), + ); + }), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: ValueListenableBuilder( + valueListenable: loading, + builder: (context, load, _) { + return LoadingButton( + loading: load, + onPressed: context + .watch() + .state is DiscountLoading + ? null + : () async { + try { + loading.value = true; + + final link = + await PaymantRepository + .getLinkPaymant( + pr.oldPrice ?? + pr.price ?? + 0, + code: discountCode); + await launchUrl(Uri.parse(link), + mode: LaunchMode + .externalApplication) + .onError( + (error, stackTrace) { + if (kDebugMode) { + print( + 'error open Link is: $error'); + } + return false; + }, + ); + } on DioException catch (e) { + SnackBarManager(context, + id: 'error-bazar-paymant') + .show( + status: + SnackBarStatus.error, + message: + 'پرداخت ناموفق بود'); + + if (kDebugMode) { + print("Dio Error is: $e"); + } + } + loading.value = false; + }, + radius: 360, + color: AppColors.primaryColor.defaultShade, + width: MediaQuery.sizeOf(context).width, + height: 48, + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + 'خرید بسته ${pr.title ?? ''}', + style: AppTextStyles.body4 + .copyWith(color: Colors.white), + ), + const SizedBox( + width: 8, + ), + Padding( + padding: const EdgeInsets.only( + bottom: 4.0), + child: Assets.icon.outline.crown.svg( + color: Colors.white, + width: 18, + ), + ), + ], + )); + }), + ), + ], + ), + if (widget.button != null) widget.button!.call() + ], + ), + ), + if (widget.label != null) + Positioned( + top: 0, left: 36, child: VerticalRibbon(text: widget.label!)), + if (pr.title != 'بسته ویژه سازمان‌ها') + Positioned( + top: 16, + right: 16, + child: GestureDetector( + onTap: () { + DialogHandler(context: context).showExtras(); + }, + child: Assets.icon.outline.infoCircle.svg( + color: AppColors.gray[ + context.read().isDark() + ? 600 + : 900], + width: 32, + height: 32), + )) + ], + ), + ), + ); + } +} diff --git a/lib/ui/widgets/components/purchase/purchase_card_placeholder.dart b/lib/ui/widgets/components/purchase/purchase_card_placeholder.dart new file mode 100644 index 0000000..a7dc473 --- /dev/null +++ b/lib/ui/widgets/components/purchase/purchase_card_placeholder.dart @@ -0,0 +1,153 @@ +import 'package:flutter/material.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/loading_button.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; + +class PurchaseCardPlaceholder extends StatefulWidget { + const PurchaseCardPlaceholder({ + super.key, + }); + + @override + State createState() => + _PurchaseCardPlaceholderState(); +} + +class _PurchaseCardPlaceholderState extends State { + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Theme.of(context).colorScheme.surface), + child: Column( + children: [ + DefaultPlaceHolder( + child: Container( + width: 94, + height: 94, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.secondryColor[50], shape: BoxShape.circle), + ), + ), + const SizedBox( + height: 8, + ), + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16)), + child: Text( + 'widget.plan.title', + style: AppTextStyles.headline5 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ), + ), + ), + const SizedBox( + height: 8, + ), + Column( + children: [ + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16)), + child: Text( + '500 سکه هوشان + 500 سکه رایگان', + style: AppTextStyles.body4.copyWith( + color: Theme.of(context).colorScheme.onSurface), + textAlign: TextAlign.center, + textDirection: TextDirection.rtl, + ), + ), + ), + const SizedBox( + height: 32, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + textDirection: TextDirection.rtl, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16)), + child: Text(':مبلغ قابل پرداخت', + style: AppTextStyles.body3.copyWith( + color: Theme.of(context) + .colorScheme + .primary)), + ), + ), + ], + ), + Row( + children: [ + DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16)), + child: Text('50000 تومان', + style: AppTextStyles.body4.copyWith( + color: AppColors.green.defaultShade)), + ), + ), + ], + ) + ], + ), + ), + const SizedBox( + height: 32, + ), + DefaultPlaceHolder( + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16)), + child: Text( + 'کد تخفیف دارید؟ اینجا کلیک کنید.', + textDirection: TextDirection.rtl, + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: LoadingButton( + loading: true, + radius: 360, + color: AppColors.gray[800], + width: MediaQuery.sizeOf(context).width, + child: Text( + 'خرید', + style: + AppTextStyles.body4.copyWith(color: Colors.white), + )), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui/widgets/components/shapes/vertical_ribbon.dart b/lib/ui/widgets/components/shapes/vertical_ribbon.dart new file mode 100644 index 0000000..050e601 --- /dev/null +++ b/lib/ui/widgets/components/shapes/vertical_ribbon.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; + +class VerticalRibbon extends StatelessWidget { + final String text; + const VerticalRibbon({super.key, required this.text}); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + RotatedBox( + quarterTurns: 3, + child: Container( + margin: const EdgeInsets.only(left: 1), + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8) + .copyWith(left: 46), + decoration: BoxDecoration( + color: AppColors.red.defaultShade, + borderRadius: BorderRadius.circular(4)), + child: Text( + text, + style: AppTextStyles.body4 + .copyWith(color: Colors.white, fontWeight: FontWeight.bold), + ), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Center( + child: ClipPath( + clipper: RoundedTriangle(), + child: Container( + height: 24, + // width: 24, + decoration: const BoxDecoration( + color: Colors.white, + ), + ), + ), + ), + ) + ], + ); + } +} + +class RoundedTriangle extends CustomClipper { + @override + Path getClip(Size size) { + final Path path = Path(); + path.lineTo(size.width / 2, 0); + path.lineTo(size.width, size.height); + path.lineTo(0, size.height); + path.lineTo(size.width / 2, 0); + path.close(); + return path; + } + + @override + bool shouldReclip(covariant CustomClipper oldClipper) { + return false; + } +} diff --git a/lib/ui/widgets/components/slider/carousle_slider_banners.dart b/lib/ui/widgets/components/slider/carousle_slider_banners.dart new file mode 100644 index 0000000..513fa7c --- /dev/null +++ b/lib/ui/widgets/components/slider/carousle_slider_banners.dart @@ -0,0 +1,161 @@ +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/data/model/banner_model.dart'; +import 'package:hoshan/data/model/chat_args.dart'; +import 'package:hoshan/ui/screens/main/home_page.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/widgets/components/image/network_image.dart'; +import 'package:hoshan/ui/widgets/components/slider/custom_carousel_controller.dart'; +import 'package:smooth_page_indicator/smooth_page_indicator.dart'; +import 'package:string_validator/string_validator.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class CarousleSliderBanners extends StatefulWidget { + final List banners; + final bool autoPlay; + final bool enableInfiniteScroll; + final Function(Banners banner)? onClick; + const CarousleSliderBanners( + {super.key, + required this.banners, + this.autoPlay = true, + this.enableInfiniteScroll = true, + this.onClick}); + + @override + State createState() => _CarousleSliderBannersState(); +} + +class _CarousleSliderBannersState extends State { + final CustomCarouselController _buttonCarouselController = + CustomCarouselController(); + + @override + Widget build(BuildContext context) { + final isWeb = + Responsive(context).isDesktop(); // Check if the platform is web + final isTab = + Responsive(context).isTablet(); // Check if the platform is tablet + final CarouselOptions carouselOptions = CarouselOptions( + viewportFraction: isWeb + ? 0.25 // Show 4 banners per slide on web + : isTab + ? 0.33 // Show 3 banners per slide on tablets + : 1, // Default to 1 banner per slide + initialPage: 0, + disableCenter: false, + enableInfiniteScroll: widget.enableInfiniteScroll, + reverse: false, + autoPlay: widget.autoPlay, + autoPlayCurve: Curves.fastOutSlowIn, + enlargeCenterPage: true, + enlargeFactor: 0, + onPageChanged: (index, _) {}, + scrollDirection: Axis.horizontal, + aspectRatio: 16 / 9, + height: isWeb + ? MediaQuery.sizeOf(context).height * 0.2 + : isTab + ? MediaQuery.sizeOf(context).height * 0.25 + : null, // Adjust height for web and tablets + ); + return Column( + children: [ + CarouselSlider.builder( + carouselController: _buttonCarouselController, + itemCount: widget.banners.length, + itemBuilder: (context, index, realIndex) { + final banner = widget.banners[index]; + return GestureDetector( + onTap: () async { + if (banner.bot != null) { + if (banner.bot?.tool ?? false) { + context.go(Routes.assistant, extra: banner.bot!.id); + } else { + context.go(Routes.chat, + extra: ChatArgs(bot: banner.bot!)); + } + } else if (banner.link != null) { + if (banner.link!.isURL()) { + await launchUrl(Uri.parse(banner.link!), + mode: LaunchMode.externalApplication) + .onError( + (error, stackTrace) { + if (kDebugMode) { + print('error open Link is: $error'); + } + return false; + }, + ); + } else { + try { + if (banner.link!.contains('navigate')) { + int? index; + if (banner.link!.contains('home')) { + index = 0; + } else if (banner.link!.contains('assisstant')) { + index = 1; + } else if (banner.link!.contains('media')) { + index = 2; + } else if (banner.link!.contains('characters')) { + index = 3; + } else if (banner.link!.contains('setting')) { + index = 4; + } + + screenIndex.value = index!; + } else { + context.go(banner.link!); + } + } catch (e) { + if (kDebugMode) { + print('Error is: $e'); + } + } + } + } + widget.onClick?.call(banner); + }, + child: sliderView(banner)); + }, + options: carouselOptions), + if (widget.banners.length > 1) sliderIndicator(), + ], + ); + } + + Widget sliderView(Banners banner) { + return Padding( + padding: const EdgeInsets.all(16), + child: ImageNetwork( + width: double.infinity, + url: banner.image ?? '', + fit: BoxFit.contain, + radius: 16, + ), + ); + } + + Widget sliderIndicator() { + return FutureBuilder( + future: _buttonCarouselController.onReady, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + _buttonCarouselController.state!.pageController!.addListener(() {}); + return SmoothPageIndicator( + controller: _buttonCarouselController.state!.pageController!, + count: widget.banners.length, + effect: ScrollingDotsEffect( + dotWidth: 8, + dotHeight: 8, + activeDotColor: AppColors.secondryColor.defaultShade, + dotColor: AppColors.secondryColor[100])); + } + return const SizedBox(); + }); + } +} diff --git a/lib/ui/widgets/components/slider/custom_carousel_controller.dart b/lib/ui/widgets/components/slider/custom_carousel_controller.dart new file mode 100644 index 0000000..14ecca3 --- /dev/null +++ b/lib/ui/widgets/components/slider/custom_carousel_controller.dart @@ -0,0 +1,14 @@ +import 'package:carousel_slider/carousel_controller.dart'; +import 'package:carousel_slider/carousel_state.dart'; + +class CustomCarouselController extends CarouselSliderControllerImpl { + CarouselState? _state; + + CarouselState? get state => _state; + + @override + set state(CarouselState? state) { + _state = state; + super.state = state; + } +} diff --git a/lib/ui/widgets/components/snackbar/snackbar_manager.dart b/lib/ui/widgets/components/snackbar/snackbar_manager.dart new file mode 100644 index 0000000..16fed29 --- /dev/null +++ b/lib/ui/widgets/components/snackbar/snackbar_manager.dart @@ -0,0 +1,136 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:another_flushbar/flushbar.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/data/snackbar_messages_model.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; + +List snackBarMessages = []; + +enum SnackBarStatus { info, success, error } + +class SnackBarManager { + BuildContext context; + String? id; + SnackBarManager(this.context, {this.id}); + + show( + {required final String message, + final SnackBarStatus? status, + Color? backgroundColor, + Color? borderColor, + final Widget? btns, + final bool isTop = true}) { + if (snackBarMessages.any( + (element) => element.id == id, + )) { + return; + } + + switch (status) { + case SnackBarStatus.success: + borderColor = AppColors.green[50]; + backgroundColor = AppColors.green.defaultShade; + break; + case SnackBarStatus.error: + borderColor = AppColors.red[50]; + backgroundColor = AppColors.red.defaultShade; + break; + case SnackBarStatus.info: + borderColor = AppColors.primaryColor[50]; + backgroundColor = AppColors.primaryColor.defaultShade; + break; + default: + borderColor = borderColor ?? Theme.of(context).colorScheme.onSurface; + backgroundColor = + backgroundColor ?? Theme.of(context).colorScheme.surface; + } + final flush = Flushbar( + textDirection: TextDirection.rtl, + backgroundColor: backgroundColor, + borderColor: borderColor, + messageText: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (btns != null) btns, + Expanded( + child: Text( + message, + style: AppTextStyles.body4.copyWith( + color: status == null + ? Theme.of(context).colorScheme.onSurface + : Colors.white), + textDirection: TextDirection.rtl, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + flushbarPosition: isTop ? FlushbarPosition.TOP : FlushbarPosition.BOTTOM, + flushbarStyle: FlushbarStyle.FLOATING, + dismissDirection: FlushbarDismissDirection.HORIZONTAL, + isDismissible: true, + animationDuration: const Duration(milliseconds: 300), + margin: const EdgeInsets.all(16), + borderRadius: BorderRadius.circular(16), + maxWidth: Responsive(context).isDesktop() + ? 800 + : Responsive(context).isTablet() + ? 400 + : null, + duration: const Duration(seconds: 3), + onStatusChanged: (status) { + if (status == FlushbarStatus.DISMISSED) { + snackBarMessages.removeWhere( + (element) => element.id == id, + ); + } + }, + )..show(context); + if (id != null) { + snackBarMessages.add(SnackbarMessagesModel(id: id!, flushbar: flush)); + } + } + + showCompleteProfileAlert({bool isTop = true}) { + show( + isTop: isTop, + status: SnackBarStatus.info, + message: 'ابتدا باید پروفایل خود را تکمیل کنید', + btns: GestureDetector( + onTap: () { + if (id != null) { + dismiss(id!); + } + context.go(Routes.editProfile); + }, + child: Text( + 'ویرایش پروفایل', + style: AppTextStyles.body6.copyWith( + color: AppColors.red.defaultShade, fontWeight: FontWeight.bold), + ), + )); + } + + static dismiss(String id) { + final message = snackBarMessages.firstWhere( + (element) => element.id == id, + ); + message.flushbar.dismiss(); + snackBarMessages.removeWhere( + (element) => element.id == id, + ); + } + + static dismissAll() { + for (final message in snackBarMessages) { + message.flushbar.dismiss(); + } + snackBarMessages.clear(); + } +} diff --git a/lib/ui/widgets/components/switch/lite_rolling_switch.dart b/lib/ui/widgets/components/switch/lite_rolling_switch.dart new file mode 100644 index 0000000..f4f1c72 --- /dev/null +++ b/lib/ui/widgets/components/switch/lite_rolling_switch.dart @@ -0,0 +1,225 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +class LiteRollingSwitch extends StatefulWidget { + @required + final bool value; + final double width; + final double heght; + + @required + final Function(bool) onChanged; + final String textOff; + final Color textOffColor; + final String textOn; + final Color textOnColor; + final Color colorOn; + final Color colorOff; + final double textSize; + final Duration animationDuration; + final Widget? iconOn; + final Widget? iconOff; + final Function()? onTap; + final Function()? onDoubleTap; + final Function()? onSwipe; + + const LiteRollingSwitch({ + super.key, + this.value = false, + this.width = 72, + this.heght = 32, + this.textOff = "Off", + this.textOn = "On", + this.textSize = 14.0, + this.colorOn = Colors.green, + this.colorOff = Colors.red, + this.iconOff, + this.iconOn, + this.animationDuration = const Duration(milliseconds: 300), + this.textOffColor = Colors.white, + this.textOnColor = Colors.black, + this.onTap, + this.onDoubleTap, + this.onSwipe, + required this.onChanged, + }); + + @override + State createState() => _RollingSwitchState(); +} + +class _RollingSwitchState extends State + with SingleTickerProviderStateMixin { + /// Late declarations + late AnimationController animationController; + late Animation animation; + late bool turnState; + + double value = 0.0; + + @override + void dispose() { + //Ensure to dispose animation controller + animationController.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + animationController = AnimationController( + vsync: this, + lowerBound: 0.0, + upperBound: 1.0, + duration: widget.animationDuration); + animation = + CurvedAnimation(parent: animationController, curve: Curves.easeInOut); + animationController.addListener(() { + setState(() { + value = animation.value; + }); + }); + turnState = widget.value; + + // Executes a function only one time after the layout is completed. + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + if (turnState) { + animationController.forward(); + } + }); + }); + } + + @override + Widget build(BuildContext context) { + //Color transition animation + Color? transitionColor = Color.lerp(widget.colorOff, widget.colorOn, value); + + return GestureDetector( + onDoubleTap: () { + _action(); + widget.onDoubleTap?.call(); + }, + onTap: () { + _action(); + widget.onTap?.call(); + }, + onPanEnd: (details) { + _action(); + widget.onSwipe?.call(); + }, + child: Container( + padding: const EdgeInsets.all(4).copyWith(left: 0), + width: widget.width, + height: widget.heght, + decoration: BoxDecoration( + color: transitionColor, borderRadius: BorderRadius.circular(50)), + child: Stack( + children: [ + Transform.translate( + offset: isRTL(context) + ? Offset(-10 * value, 0) + : Offset(10 * value, 0), //original + child: Opacity( + opacity: (1 - value).clamp(0.0, 1.0), + child: Container( + padding: isRTL(context) + ? const EdgeInsets.only(left: 10) + : const EdgeInsets.only(right: 10), + alignment: isRTL(context) + ? Alignment.centerLeft + : Alignment.centerRight, + height: 40, + child: Text( + widget.textOff, + style: TextStyle( + color: widget.textOffColor, + fontWeight: FontWeight.bold, + fontSize: widget.textSize), + ), + ), + ), + ), + Transform.translate( + offset: isRTL(context) + ? Offset(-10 * (1 - value), 0) + : Offset(10 * (1 - value), 0), //original + child: Opacity( + opacity: value.clamp(0.0, 1.0), + child: Container( + padding: isRTL(context) + ? const EdgeInsets.only(right: 5) + : const EdgeInsets.only(left: 5), + alignment: isRTL(context) + ? Alignment.centerRight + : Alignment.centerLeft, + height: 40, + child: Text( + widget.textOn, + style: TextStyle( + color: widget.textOnColor, + fontWeight: FontWeight.bold, + fontSize: widget.textSize), + ), + ), + ), + ), + Transform.translate( + offset: isRTL(context) + ? Offset((-widget.width + 50) * value, 0) + : Offset((widget.width - 40) * value, 0), + child: Transform.rotate( + angle: 0, + child: Container( + height: 40, + width: 40, + alignment: Alignment.center, + decoration: const BoxDecoration( + shape: BoxShape.circle, color: Colors.white), + child: Stack( + children: [ + Center( + child: Opacity( + opacity: (1 - value).clamp(0.0, 1.0), + child: widget.iconOff ?? const SizedBox(), + ), + ), + Center( + child: Opacity( + opacity: (value).clamp(0.0, 1.0), + child: widget.iconOn ?? const SizedBox(), + ), + ), + ], + ), + ), + ), + ) + ], + ), + ), + ); + } + + _action() { + _determine(changeState: true); + } + + _determine({bool changeState = false}) { + setState(() { + if (changeState) turnState = !turnState; + (turnState) + ? animationController.forward() + : animationController.reverse(); + + widget.onChanged(turnState); + }); + } +} + +bool isRTL(BuildContext context) { + return Bidi.isRtlLanguage(Localizations.localeOf(context).languageCode); +} diff --git a/lib/ui/widgets/components/text/auth_text_field.dart b/lib/ui/widgets/components/text/auth_text_field.dart new file mode 100644 index 0000000..8d83ff0 --- /dev/null +++ b/lib/ui/widgets/components/text/auth_text_field.dart @@ -0,0 +1,154 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hoshan/core/utils/strings.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; + +class AuthTextField extends StatefulWidget { + final TextEditingController? controller; + final Function(String)? onChange; + final String? hintText; + final int? maxLength; + final String? label; + final Widget? suffix; + final Widget? error; + final Widget? success; + final bool isPassword; + final bool enabled; + final bool justEnglish; + final int? minLines; + final int maxLines; + final TextInputAction textInputAction; + const AuthTextField( + {super.key, + this.hintText, + this.maxLength, + this.label, + this.suffix, + this.error, + this.isPassword = false, + this.controller, + this.onChange, + this.minLines, + this.textInputAction = TextInputAction.done, + this.justEnglish = false, + this.maxLines = 1, + this.success, + this.enabled = true}); + + @override + State createState() => _AuthTextFieldState(); +} + +class _AuthTextFieldState extends State { + ValueNotifier isPassword = ValueNotifier(true); + String text = ''; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: isPassword, + builder: (context, value, _) { + return Directionality( + textDirection: TextDirection.rtl, + child: TextField( + maxLength: widget.maxLength, + obscureText: widget.isPassword && value, + style: AppTextStyles.body5.copyWith( + color: widget.enabled + ? Theme.of(context).colorScheme.onSurface + : AppColors.gray[700]), + controller: widget.controller, + textInputAction: widget.minLines != null && widget.minLines! > 1 + ? TextInputAction.newline + : widget.textInputAction, + onChanged: (value) { + setState(() { + text = value; + }); + widget.onChange?.call(value); + }, + inputFormatters: [ + TextInputFormatter.withFunction((oldValue, newValue) { + if (newValue.text.isEmpty) { + return newValue; + } + if (widget.justEnglish && + !newValue.text.containsOnlyEnglish()) { + SnackBarManager(context, id: 'justTypeEnglish').show( + status: SnackBarStatus.info, + message: 'نام کاربری باید فقط شامل حروف انگلیسی باشد', + ); + + return oldValue; + } + return newValue; + }), + ], + minLines: widget.minLines, + maxLines: widget.maxLines, + textDirection: text.startsWithEnglish() + ? TextDirection.ltr + : TextDirection.rtl, + decoration: InputDecoration( + alignLabelWithHint: true, + hintText: widget.hintText, + hintStyle: AppTextStyles.body5, + contentPadding: const EdgeInsets.all(18), + error: widget.error ?? widget.success, + enabled: widget.enabled, + labelStyle: AppTextStyles.body5 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + floatingLabelStyle: AppTextStyles.body5 + .copyWith(color: Theme.of(context).colorScheme.primary), + labelText: widget.label, + suffixIcon: widget.isPassword + ? GestureDetector( + child: Icon( + value ? CupertinoIcons.eye_slash : CupertinoIcons.eye, + color: widget.error != null + ? AppColors.red.defaultShade + : Theme.of(context).colorScheme.primary, + ), + onTap: () { + isPassword.value = !isPassword.value; + }, + ) + : widget.suffix, + fillColor: Theme.of(context).colorScheme.primary, + focusColor: Theme.of(context).colorScheme.primary, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary)), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary)), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + width: 2)), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: widget.success != null + ? AppColors.green.defaultShade + : AppColors.red.defaultShade, + width: 2)), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: widget.success != null + ? AppColors.green.defaultShade + : AppColors.red.defaultShade, + width: 2)), + ), + ), + ); + }); + } +} diff --git a/lib/ui/widgets/components/text/card_number_input.dart b/lib/ui/widgets/components/text/card_number_input.dart new file mode 100644 index 0000000..85686ae --- /dev/null +++ b/lib/ui/widgets/components/text/card_number_input.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; + +class CardNumberInput extends StatefulWidget { + final Function(String) onChange; + final String initialValue; // New parameter + + const CardNumberInput({ + super.key, + required this.onChange, + this.initialValue = '', // Default to empty string + }); + + @override + State createState() => _CardNumberInputState(); +} + +class _CardNumberInputState extends State { + final List _controllers = + List.generate(4, (_) => TextEditingController()); + final List _focusNodes = List.generate(4, (_) => FocusNode()); + + @override + void initState() { + super.initState(); + _setInitialValue(widget.initialValue); // Set initial value + } + + void _setInitialValue(String value) { + // Split the initial value into segments of 4 + final segments = List.generate(4, (index) { + return value.length > index * 4 + ? value.substring(index * 4, (index + 1) * 4).padRight(4, ' ') + : ''; + }); + + for (int i = 0; i < _controllers.length; i++) { + _controllers[i].text = segments[i]; + } + } + + @override + void dispose() { + for (var controller in _controllers) { + controller.dispose(); + } + for (var focusNode in _focusNodes) { + focusNode.dispose(); + } + super.dispose(); + } + + void _onChanged(String value, int index) { + if (value.length == 4 && index < 3) { + FocusScope.of(context).requestFocus(_focusNodes[index + 1]); + } else if (value.isEmpty && index > 0) { + FocusScope.of(context).requestFocus(_focusNodes[index - 1]); + } + String result = ''; + for (var controller in _controllers) { + result += controller.text; + } + widget.onChange(result); + } + + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.ltr, + child: Row( + children: List.generate(4, (index) { + return Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: SizedBox( + height: 38, + child: TextField( + controller: _controllers[index], + focusNode: _focusNodes[index], + maxLength: 4, + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + textAlign: TextAlign.center, + style: AppTextStyles.body4 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + decoration: InputDecoration( + counterText: "", + fillColor: Theme.of(context).colorScheme.surface, + filled: true, + border: OutlineInputBorder( + borderSide: BorderSide(color: AppColors.gray[50]), + borderRadius: BorderRadius.circular(8)), + contentPadding: EdgeInsets.zero), + onChanged: (value) => _onChanged(value, index), + ), + ), + ), + ); + }), + ), + ); + } +} diff --git a/lib/ui/widgets/components/text/credit_cost.dart b/lib/ui/widgets/components/text/credit_cost.dart new file mode 100644 index 0000000..8e086d7 --- /dev/null +++ b/lib/ui/widgets/components/text/credit_cost.dart @@ -0,0 +1,61 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:hoshan/data/model/ai/credit_model.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/theme/text.dart'; + +class CreditCost extends StatefulWidget { + final Color? textColor; + final Color? loadingColor; + final bool call; + const CreditCost( + {super.key, this.textColor, this.call = true, this.loadingColor}); + + @override + State createState() => _CreditCostState(); +} + +class _CreditCostState extends State { + @override + void initState() { + super.initState(); + + // if (widget.call) { + context.read().getUserInfo().then( + (value) { + context.read().changeCredit(CreditModel( + credit: UserInfoCubit.userInfoModel.credit, + freeCredit: UserInfoCubit.userInfoModel.freeCredit)); + }, + ); + // } + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is UserInfoInitial || state is UserInfoLoading) { + return SpinKitThreeBounce( + color: + widget.loadingColor ?? Theme.of(context).colorScheme.onSurface, + size: 18, + ); + } + return Text( + ((UserInfoCubit.userInfoModel.credit ?? 0) + + (UserInfoCubit.userInfoModel.freeCredit ?? 0) + + (UserInfoCubit.userInfoModel.gift_credit ?? 0)) + .toString(), + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: + widget.textColor ?? Theme.of(context).colorScheme.onSurface), + ); + }, + ); + } +} diff --git a/lib/ui/widgets/components/text/default_markdown_text.dart b/lib/ui/widgets/components/text/default_markdown_text.dart new file mode 100644 index 0000000..eb43486 --- /dev/null +++ b/lib/ui/widgets/components/text/default_markdown_text.dart @@ -0,0 +1,201 @@ +// ignore_for_file: deprecated_member_use_from_same_package, use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_highlight/flutter_highlight.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/utils/strings.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; +import 'package:hoshan/ui/widgets/components/text/latex.dart'; +import 'package:markdown_widget/markdown_widget.dart'; +import 'package:flutter_highlight/themes/atom-one-dark-reasonable.dart'; +import 'package:flutter_highlight/themes/atom-one-light.dart'; + +class DefaultMarkdownText extends StatefulWidget { + final String text; + final Color? color; + final double? width; + final bool fromBot; + const DefaultMarkdownText( + {super.key, + required this.text, + this.color, + this.width, + this.fromBot = false}); + + @override + State createState() => DefaultMarkdownTextState(); +} + +class DefaultMarkdownTextState extends State { + late TextDirection textDirection = + widget.text.startsWithEnglish() ? TextDirection.ltr : TextDirection.rtl; + + String changeDirection() { + setState(() { + if (textDirection == TextDirection.rtl) { + textDirection = TextDirection.ltr; + } else { + textDirection = TextDirection.rtl; + } + }); + return textDirection.name.toUpperCase(); + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + final config = isDark + ? MarkdownConfig.darkConfig.copy(configs: [ + CodeConfig( + style: MarkdownConfig.darkConfig.code.style.copyWith( + color: widget.color, + backgroundColor: + atomOneDarkReasonableTheme['root']!.backgroundColor)), + ]) + : MarkdownConfig.defaultConfig; + return Column( + children: [ + Directionality( + textDirection: textDirection, + child: SizedBox( + width: widget.width, + child: MarkdownBlock( + data: widget.text, + selectable: true, + generator: MarkdownGenerator( + generators: [latexGenerator], + inlineSyntaxList: [LatexSyntax()], + // richTextBuilder: (span) => Text.rich( + // span, + // softWrap: true, + // style: config.p.textStyle.copyWith( + // fontFamily: AppTextStyles.defaultFontFamily, + // color: widget.color), + // ), + ), + config: config.copy(configs: [ + PreConfig( + builder: (code, language) => Padding( + padding: const EdgeInsets.symmetric(vertical: 18.0), + child: Directionality( + textDirection: TextDirection.ltr, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isDark + ? AppColors.black[900] + : AppColors.primaryColor.defaultShade), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + language, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 16), + ), + InkWell( + onTap: () async { + await Clipboard.setData( + ClipboardData(text: code)); + if (mounted) { + SnackBarManager(context, id: 'Copy') + .show( + status: SnackBarStatus.success, + message: 'پیام با موفقیت کپی شد 😃', + ); + } + }, + child: Row( + children: [ + const Text( + 'copy', + style: TextStyle( + color: Colors.white, + fontSize: 14), + ), + const SizedBox( + width: 4, + ), + Assets.icon.outline.copy + .svg(color: Colors.white), + ], + ), + ) + ], + ), + ), + Container( + constraints: const BoxConstraints( + minWidth: double.infinity), + child: HighlightView( + code, + language: language, + theme: isDark + ? atomOneDarkReasonableTheme + : atomOneLightTheme, + textStyle: + TextStyle(color: widget.color, height: 1.4), + padding: const EdgeInsets.all(8), + ), + ), + ], + ), + ), + ), + ), + ), + H1Config( + style: config.h1.style.copyWith( + fontSize: 22, + fontFamily: AppTextStyles.defaultFontFamily, + color: widget.color)), + H2Config( + style: config.h2.style.copyWith( + fontSize: 21, + fontFamily: AppTextStyles.defaultFontFamily, + color: widget.color)), + H3Config( + style: config.h3.style.copyWith( + fontSize: 20, + fontFamily: AppTextStyles.defaultFontFamily, + color: widget.color)), + H4Config( + style: config.h4.style.copyWith( + fontSize: 19, + fontFamily: AppTextStyles.defaultFontFamily, + color: widget.color)), + H5Config( + style: config.h5.style.copyWith( + fontSize: 18, + fontFamily: AppTextStyles.defaultFontFamily, + color: widget.color)), + H6Config( + style: config.h6.style.copyWith( + fontSize: 17, + fontFamily: AppTextStyles.defaultFontFamily, + color: widget.color)), + PConfig( + textStyle: config.p.textStyle.copyWith( + fontSize: 16, + fontFamily: AppTextStyles.defaultFontFamily, + color: widget.color), + ), + ]), + ), + ), + ), + ], + ); + } +} diff --git a/lib/ui/widgets/components/text/filled_text_field.dart b/lib/ui/widgets/components/text/filled_text_field.dart new file mode 100644 index 0000000..05297cb --- /dev/null +++ b/lib/ui/widgets/components/text/filled_text_field.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:hoshan/core/utils/strings.dart'; +import 'package:hoshan/data/model/edittext_state_model.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; + +class FilledTextField extends StatefulWidget { + final Function(String)? onChange; + final String? Function(String?)? onValid; + final EdittextStateModel? stateController; + + final String? hintText; + + const FilledTextField( + {super.key, + this.onChange, + this.hintText, + this.onValid, + this.stateController}); + + @override + State createState() => _FilledTextFieldState(); +} + +class _FilledTextFieldState extends State { + String text = ''; + bool resetError = false; + + @override + Widget build(BuildContext context) { + final defaultBorder = OutlineInputBorder( + borderRadius: BorderRadius.circular(4), + borderSide: BorderSide(color: AppColors.gray[600])); + return Directionality( + textDirection: TextDirection.rtl, + child: Form( + key: widget.stateController?.formState, + child: TextFormField( + controller: widget.stateController?.formController, + validator: (value) { + if (resetError) { + resetError = false; + return null; + } + return widget.onValid?.call(value); + }, + textDirection: + text.startsWithEnglish() ? TextDirection.ltr : TextDirection.rtl, + style: AppTextStyles.body4 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + decoration: InputDecoration( + border: defaultBorder, + errorBorder: defaultBorder, + enabledBorder: defaultBorder, + // focusedBorder: defaultBorder, + disabledBorder: defaultBorder, + // focusedErrorBorder: defaultBorder, + hintText: widget.hintText, + fillColor: Theme.of(context).colorScheme.surface, + filled: true, + hintStyle: + AppTextStyles.body4.copyWith(color: AppColors.gray[700]), + errorStyle: AppTextStyles.body4), + onChanged: (value) { + widget.onChange?.call(value); + setState(() { + text = value; + resetError = true; + widget.stateController?.formState.currentState?.validate(); + }); + }, + ), + ), + ); + } +} diff --git a/lib/ui/widgets/components/text/labeled_text_field.dart b/lib/ui/widgets/components/text/labeled_text_field.dart new file mode 100644 index 0000000..b80cbf5 --- /dev/null +++ b/lib/ui/widgets/components/text/labeled_text_field.dart @@ -0,0 +1,170 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hoshan/core/utils/strings.dart'; +import 'package:hoshan/data/model/edittext_state_model.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/dropdown/hint_tooltip.dart'; +import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; + +class LabeledTextField extends StatefulWidget { + final EdittextStateModel? stateController; + final Function(String)? onChange; + final String? Function(String?)? onValid; + final int? maxLength; + final TextInputAction textInputAction; + + final Widget? suffix; + final Widget? error; + final Widget? success; + final bool isPassword; + final bool enabled; + final int? minLines; + final int? maxLines; + final TextStyle? hintStyle; + final bool showLabel; + final bool justEnglish; + const LabeledTextField( + {super.key, + this.onChange, + this.maxLength, + this.suffix, + this.error, + this.success, + this.isPassword = false, + this.enabled = true, + this.minLines, + this.maxLines, + this.onValid, + this.stateController, + this.hintStyle, + this.textInputAction = TextInputAction.done, + this.showLabel = true, + this.justEnglish = false}); + + @override + State createState() => _LabeledTextFieldState(); +} + +class _LabeledTextFieldState extends State { + String text = ''; + bool resetError = false; + bool isPassword = false; + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.rtl, + child: Form( + key: widget.stateController?.formState, + child: TextFormField( + textInputAction: widget.minLines != null && widget.minLines! > 1 + ? TextInputAction.newline + : widget.textInputAction, + controller: widget.stateController?.formController, + onChanged: (value) { + widget.onChange?.call(value); + setState(() { + text = value; + resetError = true; + widget.stateController?.formState.currentState?.validate(); + }); + }, + validator: (value) { + if (resetError) { + resetError = false; + return null; + } + return widget.onValid?.call(value); + }, + inputFormatters: widget.justEnglish + ? [ + TextInputFormatter.withFunction((oldValue, newValue) { + if (!newValue.text.containsOnlyEnglish()) { + SnackBarManager(context, id: 'justTypeEnglish').show( + status: SnackBarStatus.info, + message: 'نام کاربری باید فقط شامل حروف انگلیسی باشد', + ); + + return oldValue; + } + return newValue; + }), + ] + : null, + maxLength: widget.maxLength, + obscureText: widget.isPassword && isPassword, + style: AppTextStyles.body4 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + minLines: widget.minLines, + maxLines: widget.maxLines, + textDirection: + text.startsWithEnglish() ? TextDirection.ltr : TextDirection.rtl, + decoration: InputDecoration( + labelStyle: AppTextStyles.body4 + .copyWith(color: AppColors.primaryColor.defaultShade), + errorStyle: AppTextStyles.body4.copyWith( + color: AppColors.red.defaultShade, fontWeight: FontWeight.bold), + hintText: widget.stateController?.hintText, + hintStyle: widget.hintStyle ?? AppTextStyles.body5, + contentPadding: const EdgeInsets.all(18), + error: widget.error ?? widget.success, + enabled: widget.enabled, + floatingLabelBehavior: + FloatingLabelBehavior.always, // Add this line + label: widget.stateController?.label != null + ? Opacity( + opacity: widget.showLabel ? 1 : 0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.stateController?.tooltipHint != null) + Row( + children: [ + HintTooltip( + hint: widget.stateController!.tooltipHint!), + const SizedBox( + width: 12, + ), + ], + ), + Text( + widget.stateController?.label ?? '', + style: AppTextStyles.body2.copyWith( + color: Theme.of(context).colorScheme.primary), + ), + if (!widget.showLabel) + const SizedBox( + width: 16, + ), + ], + ), + ) + : null, + + fillColor: AppColors.primaryColor.defaultShade, + focusColor: AppColors.primaryColor.defaultShade, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: + BorderSide(color: AppColors.primaryColor.defaultShade)), + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: widget.success != null + ? AppColors.green.defaultShade + : AppColors.red.defaultShade, + width: 2)), + errorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: widget.success != null + ? AppColors.green.defaultShade + : AppColors.red.defaultShade, + width: 2)), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: AppColors.primaryColor.defaultShade, width: 2)), + ), + ), + ), + ); + } +} diff --git a/lib/ui/widgets/components/text/latex.dart b/lib/ui/widgets/components/text/latex.dart new file mode 100644 index 0000000..8c29446 --- /dev/null +++ b/lib/ui/widgets/components/text/latex.dart @@ -0,0 +1,97 @@ +// ignore_for_file: depend_on_referenced_packages + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:markdown_widget/markdown_widget.dart'; +import 'package:flutter_math_fork/flutter_math.dart'; +import 'package:markdown/markdown.dart' as m; + +SpanNodeGeneratorWithTag latexGenerator = SpanNodeGeneratorWithTag( + tag: _latexTag, + generator: (e, config, visitor) => + LatexNode(e.attributes, e.textContent, config)); + +const _latexTag = 'latex'; + +class LatexSyntax extends m.InlineSyntax { + LatexSyntax() : super(r'(\$\$[\s\S]+\$\$)|(\$.+?\$)'); + + @override + bool onMatch(m.InlineParser parser, Match match) { + final input = match.input; + final matchValue = input.substring(match.start, match.end); + String content = ''; + bool isInline = true; + + const blockSyntax = '\$\$'; + const inlineSyntax = '\$'; + if (matchValue.startsWith(blockSyntax) && + matchValue.endsWith(blockSyntax) && + (matchValue != blockSyntax)) { + content = matchValue.substring(2, matchValue.length - 2); + isInline = false; + } else if (matchValue.startsWith(inlineSyntax) && + matchValue.endsWith(inlineSyntax) && + matchValue != inlineSyntax) { + content = matchValue.substring(1, matchValue.length - 1); + } + m.Element el = m.Element.text(_latexTag, matchValue); + el.attributes['content'] = content; + el.attributes['isInline'] = '$isInline'; + parser.addNode(el); + return true; + } +} + +class LatexNode extends SpanNode { + final Map attributes; + final String textContent; + final MarkdownConfig config; + + LatexNode(this.attributes, this.textContent, this.config); + + @override + InlineSpan build() { + final content = attributes['content'] ?? ''; + final isInline = attributes['isInline'] == 'true'; + final style = parentStyle ?? config.code.style; + if (content.isEmpty) return TextSpan(style: style, text: textContent); + final latex = Math.tex( + content, + mathStyle: MathStyle.text, + textScaleFactor: 1, + textStyle: config.code.style.copyWith( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + onErrorFallback: (error) { + if (kDebugMode) { + print("Error is: $error"); + } + return Text( + textContent, + style: style, + ); + }, + ); + return WidgetSpan( + alignment: PlaceholderAlignment.middle, + style: style, + child: Directionality( + textDirection: TextDirection.ltr, // Change text direction here + + child: !isInline + ? Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: config.code.style.backgroundColor, + borderRadius: BorderRadius.circular(10)), + padding: + const EdgeInsets.symmetric(horizontal: 2, vertical: 24), + child: Center(child: Flexible(child: latex)), + ) + : latex, + )); + } +} diff --git a/lib/ui/widgets/components/text/mobile_number_text_field.dart b/lib/ui/widgets/components/text/mobile_number_text_field.dart new file mode 100644 index 0000000..919d522 --- /dev/null +++ b/lib/ui/widgets/components/text/mobile_number_text_field.dart @@ -0,0 +1,129 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hoshan/core/utils/strings.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; + +class MobileNumberTextField extends StatefulWidget { + final TextEditingController? controller; + final Function(String)? onChange; + final String? hintText; + final int? maxLength; + final String? label; + final Widget? suffix; + final Widget? error; + final Widget? success; + final bool isPassword; + final bool enabled; + final bool justEnglish; + final int? minLines; + final int maxLines; + final TextInputAction textInputAction; + const MobileNumberTextField( + {super.key, + this.hintText, + this.maxLength, + this.label, + this.suffix, + this.error, + this.isPassword = false, + this.controller, + this.onChange, + this.minLines, + this.textInputAction = TextInputAction.done, + this.justEnglish = false, + this.maxLines = 1, + this.success, + this.enabled = true}); + + @override + State createState() => _MobileNumberTextFieldState(); +} + +class _MobileNumberTextFieldState extends State { + ValueNotifier isPassword = ValueNotifier(true); + String text = ''; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: isPassword, + builder: (context, value, _) { + return Directionality( + textDirection: TextDirection.rtl, + child: TextField( + maxLength: widget.maxLength, + obscureText: widget.isPassword && value, + style: AppTextStyles.body5.copyWith( + color: widget.enabled + ? Theme.of(context).colorScheme.onSurface + : AppColors.gray[700]), + controller: widget.controller, + textInputAction: widget.minLines != null && widget.minLines! > 1 + ? TextInputAction.newline + : widget.textInputAction, + onChanged: (value) { + setState(() { + text = value; + }); + widget.onChange?.call(value); + }, + keyboardType: TextInputType.phone, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + minLines: widget.minLines, + maxLines: widget.maxLines, + textDirection: text.startsWithEnglish() + ? TextDirection.ltr + : TextDirection.ltr, + decoration: InputDecoration( + alignLabelWithHint: true, + hintText: widget.hintText, + hintStyle: AppTextStyles.body5, + contentPadding: const EdgeInsets.all(18), + error: widget.error ?? widget.success, + enabled: widget.enabled, + labelStyle: AppTextStyles.body5 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + floatingLabelStyle: AppTextStyles.body5 + .copyWith(color: Theme.of(context).colorScheme.primary), + labelText: widget.label, + suffixIcon: Icon( + CupertinoIcons.phone, + color: Theme.of(context).colorScheme.primary, + ), + fillColor: Theme.of(context).colorScheme.primary, + focusColor: Theme.of(context).colorScheme.primary, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary)), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary)), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + width: 2)), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: widget.success != null + ? AppColors.green.defaultShade + : AppColors.red.defaultShade, + width: 2)), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: widget.success != null + ? AppColors.green.defaultShade + : AppColors.red.defaultShade, + width: 2)), + ), + ), + ); + }); + } +} diff --git a/lib/ui/widgets/components/text/search_text_field.dart b/lib/ui/widgets/components/text/search_text_field.dart new file mode 100644 index 0000000..9d50930 --- /dev/null +++ b/lib/ui/widgets/components/text/search_text_field.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/text.dart'; + +class SearchTextField extends StatelessWidget { + final Function(String)? onChanged; + final String? hintText; + final String? label; + final Widget? suffixIcon; + final TextEditingController? controller; + final FocusNode? focusNode; + SearchTextField( + {super.key, + this.onChanged, + this.hintText, + this.label, + this.suffixIcon, + this.controller, + this.focusNode}); + + final OutlineInputBorder outlineInputBorder = OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: AppColors.gray.defaultShade)); + + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.rtl, + child: Container( + color: Theme.of(context).colorScheme.surface, + margin: const EdgeInsets.symmetric(horizontal: 12), + child: TextField( + focusNode: focusNode, + controller: controller, + style: AppTextStyles.body5 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + onChanged: onChanged, + decoration: InputDecoration( + hintText: hintText ?? 'جستجو', + hintStyle: AppTextStyles.body5.copyWith(), + contentPadding: const EdgeInsets.all(18), + label: Text( + label ?? 'جستجو', + style: AppTextStyles.body5, + ), + suffixIcon: suffixIcon, + prefixIcon: Padding( + padding: const EdgeInsets.all(12.0), + child: Assets.icon.outline.searchNormal.svg(), + ), + fillColor: AppColors.primaryColor.defaultShade, + focusColor: AppColors.primaryColor.defaultShade, + border: outlineInputBorder, + errorBorder: outlineInputBorder.copyWith( + borderSide: + BorderSide(color: AppColors.red.defaultShade, width: 2)), + focusedBorder: outlineInputBorder.copyWith( + borderSide: BorderSide( + color: AppColors.primaryColor.defaultShade, width: 2))), + ), + ), + ); + } +} diff --git a/lib/ui/widgets/components/video/chat_video_player.dart b/lib/ui/widgets/components/video/chat_video_player.dart new file mode 100644 index 0000000..ec3593a --- /dev/null +++ b/lib/ui/widgets/components/video/chat_video_player.dart @@ -0,0 +1,79 @@ +// ignore_for_file: deprecated_member_use + +import 'package:chewie/chewie.dart'; + +import 'package:flutter/material.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; +import 'package:video_player/video_player.dart'; + +class ChatVideoPlayer extends StatefulWidget { + final String src; + final Widget? custome; + final bool showOptions; + const ChatVideoPlayer( + {super.key, required this.src, this.custome, this.showOptions = false}); + + @override + State createState() => _ChatVideoPlayerState(); +} + +class _ChatVideoPlayerState extends State { + late VideoPlayerController _videoPlayerController; + ChewieController? _chewieController; + + @override + void initState() { + super.initState(); + _handleVideoPlayback(); + } + + Future _handleVideoPlayback() async { + _videoPlayerController = VideoPlayerController.network(widget.src); + + await _videoPlayerController.initialize().then((_) { + setState(() { + _chewieController = ChewieController( + customControls: widget.custome, + videoPlayerController: _videoPlayerController, + autoPlay: false, + looping: true, + showOptions: widget.showOptions, + allowPlaybackSpeedChanging: widget.showOptions, + aspectRatio: 16 / 9, + materialProgressColors: ChewieProgressColors( + playedColor: Theme.of(context).colorScheme.primary, + handleColor: Theme.of(context).colorScheme.primary), + ); + }); + }).catchError((e) { + setState(() {}); + }); + } + + @override + void dispose() { + _videoPlayerController.pause(); + _videoPlayerController.dispose(); + _chewieController?.dispose(); // Dispose of the ChewieController + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _chewieController == null + ? DefaultPlaceHolder( + child: Container( + width: MediaQuery.sizeOf(context).width, + height: 200, + decoration: BoxDecoration( + color: Colors.white, borderRadius: BorderRadius.circular(16)), + ), + ) + : AspectRatio( + aspectRatio: _videoPlayerController.value.aspectRatio, + child: Chewie( + controller: _chewieController!, + ), + ); + } +} diff --git a/lib/ui/widgets/components/video/custome_controls.dart b/lib/ui/widgets/components/video/custome_controls.dart new file mode 100644 index 0000000..25b314a --- /dev/null +++ b/lib/ui/widgets/components/video/custome_controls.dart @@ -0,0 +1,197 @@ +import 'dart:async'; + +import 'package:chewie/chewie.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:hoshan/ui/widgets/components/video/play_btn_animation.dart'; + +class CustomControls extends StatefulWidget { + const CustomControls({super.key}); + + @override + State createState() => _CustomControlsState(); +} + +class _CustomControlsState extends State { + late ChewieController chewieController; + bool isAnimating = false; + double opacity = 1; + Timer? _hideControlsTimer; + ValueNotifier position = ValueNotifier(Duration.zero); + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + chewieController = ChewieController.of(context); + chewieController.videoPlayerController.addListener( + () { + position.value = chewieController.videoPlayerController.value.position; + }, + ); + } + + void _startHideControlsTimer() { + _hideControlsTimer?.cancel(); + _hideControlsTimer = Timer(const Duration(seconds: 3), () { + setState(() { + opacity = 0; + }); + }); + } + + @override + void dispose() { + _hideControlsTimer?.cancel(); // Clean up the timer + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.ltr, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 400), + opacity: opacity, + child: InkWell( + onTap: () { + setState(() { + opacity = 1; + }); + _startHideControlsTimer(); // Restart the timer on tap + }, + child: Stack( + children: [ + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + padding: const EdgeInsets.only(bottom: 12), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.black, + Colors.black26, + Color.fromARGB(10, 0, 0, 0) + ])), + child: Row( + children: [ + // Positioned.fill( + // child: Container( + // decoration: BoxDecoration( + // color: !chewieController.isPlaying + // ? Colors.black.withOpacity(0.4) + // : Colors.transparent), + // )), + _buildPlayPause(), + + _buildProgressIndicator(), + _buildFullScreenToggle(), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildPlayPause() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: InkWell( + onTap: () { + setState(() { + if (chewieController.isPlaying) { + chewieController.pause(); + opacity = 1; + } else { + chewieController.play(); + opacity = 0; + } + + isAnimating = true; + }); + _startHideControlsTimer(); // Restart the timer on tap + }, + child: PlayBtnAnimation( + alwaysAnimate: true, + isAnimating: isAnimating, + onEnd: () => setState( + () => isAnimating = false, + ), + child: Icon( + chewieController.isPlaying + ? CupertinoIcons.pause_fill + : CupertinoIcons.play_fill, + color: Colors.white, + size: 24, + ), + ), + ), + ); + } + + Widget _buildProgressIndicator() { + return Expanded( + child: ValueListenableBuilder( + valueListenable: position, + builder: (context, p, _) { + Duration duration = + chewieController.videoPlayerController.value.duration; + + return SliderTheme( + data: SliderThemeData( + trackHeight: 2, + // thumbColor: Colors.transparent, + overlayShape: SliderComponentShape.noOverlay, + thumbShape: const RoundSliderThumbShape( + // elevation: 0, + // pressedElevation: 0, + enabledThumbRadius: 8)), + child: Slider( + min: 0, + max: duration.inMilliseconds.toDouble(), + value: p.inMilliseconds.toDouble(), + onChanged: (value) async { + await chewieController.pause(); + position.value = Duration(milliseconds: value.round()); + _startHideControlsTimer(); + }, + onChangeEnd: (value) async { + await chewieController + .seekTo(Duration(milliseconds: value.round())); + await chewieController.play(); + + setState(() {}); + }, + ), + ); + }, + ), + ); + } + + Widget _buildFullScreenToggle() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: InkWell( + onTap: () => setState(() { + chewieController.toggleFullScreen(); + _startHideControlsTimer(); // Restart the timer on tap + }), + child: Icon( + chewieController.isFullScreen + ? Icons.fullscreen_exit + : Icons.fullscreen, + color: Colors.white, + size: 30, + ), + ), + ); + } +} diff --git a/lib/ui/widgets/components/video/play_btn_animation.dart b/lib/ui/widgets/components/video/play_btn_animation.dart new file mode 100644 index 0000000..7146239 --- /dev/null +++ b/lib/ui/widgets/components/video/play_btn_animation.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +class PlayBtnAnimation extends StatefulWidget { + final Widget child; + final bool isAnimating; + final bool alwaysAnimate; + final Duration duration; + final Function()? onEnd; + const PlayBtnAnimation( + {super.key, + required this.child, + required this.isAnimating, + this.duration = const Duration(milliseconds: 150), + this.onEnd, + this.alwaysAnimate = false}); + + @override + State createState() => _PlayBtnAnimationState(); +} + +class _PlayBtnAnimationState extends State + with SingleTickerProviderStateMixin { + late AnimationController controller; + late Animation scale; + + @override + void initState() { + super.initState(); + + final halfDuration = widget.duration.inMilliseconds ~/ 2; + controller = AnimationController( + vsync: this, duration: Duration(milliseconds: halfDuration)); + + scale = Tween(begin: 1, end: 1.2).animate(controller); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(covariant PlayBtnAnimation oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.isAnimating != oldWidget.isAnimating) { + doAnimation(); + } + } + + Future doAnimation() async { + if (widget.isAnimating) { + await controller.forward(); + await controller.reverse(); + await Future.delayed(const Duration(milliseconds: 400)); + widget.onEnd?.call(); + } + } + + @override + Widget build(BuildContext context) { + return ScaleTransition(scale: scale, child: widget.child); + } +} diff --git a/lib/ui/widgets/components/video/primary_controls.dart b/lib/ui/widgets/components/video/primary_controls.dart new file mode 100644 index 0000000..aff0781 --- /dev/null +++ b/lib/ui/widgets/components/video/primary_controls.dart @@ -0,0 +1,416 @@ +import 'dart:async'; + +import 'package:animated_custom_dropdown/custom_dropdown.dart'; +import 'package:chewie/chewie.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:hoshan/core/utils/date_time.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/video/play_btn_animation.dart'; + +class PrimaryControls extends StatefulWidget { + const PrimaryControls({super.key}); + + @override + State createState() => _PrimaryControlsState(); +} + +class _PrimaryControlsState extends State { + late ChewieController chewieController; + bool isAnimating = false; + bool isAnimatingForward = false; + bool isAnimatingBackward = false; + // bool isSpeedMenuOpen = false; + + double opacity = 1; + Timer? _hideControlsTimer; + ValueNotifier position = ValueNotifier(Duration.zero); + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + chewieController = ChewieController.of(context); + chewieController.videoPlayerController.addListener( + () { + position.value = chewieController.videoPlayerController.value.position; + }, + ); + } + + void _startHideControlsTimer() { + _hideControlsTimer?.cancel(); + _hideControlsTimer = Timer(const Duration(seconds: 5), () { + setState(() { + opacity = 0; + isAnimating = false; + // if (isSpeedMenuOpen) { + // Navigator.pop(context); + // } + }); + }); + } + + @override + void dispose() { + _hideControlsTimer?.cancel(); // Clean up the timer + super.dispose(); + } + + void _handlePlay() { + { + setState(() { + if (chewieController.isPlaying) { + chewieController.pause(); + opacity = 1; + } else { + chewieController.play(); + opacity = 0; + } + isAnimating = true; + }); + _startHideControlsTimer(); // Restart the timer on tap + } + } + + void onClickScreen() { + if (opacity == 0) { + setState(() { + opacity = 1; + }); + _startHideControlsTimer(); // Restart the timer on tap + } else { + setState(() { + opacity = 0; + }); + } + } + + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.ltr, + child: Stack( + children: [ + forwardBtn(), + backwardBtn(), + AnimatedOpacity( + duration: const Duration(milliseconds: 400), + opacity: opacity, + child: Stack( + children: [ + seekPositions(), + IgnorePointer( + ignoring: opacity == 0, + child: Stack( + children: [ + forground(), + playAndPause(), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } + + Positioned seekPositions() { + return Positioned.fill( + child: Row( + children: [ + Expanded( + child: InkWell( + onTap: onClickScreen, + onDoubleTap: () async { + if (position.value.inSeconds < 1) return; + setState(() => isAnimatingBackward = true); + Duration backward = Duration(seconds: position.value.inSeconds - 1); + await chewieController.videoPlayerController.seekTo(backward); + }, + )), + Expanded( + child: InkWell( + onTap: onClickScreen, + onDoubleTap: () async { + if (position.value.inSeconds > + chewieController + .videoPlayerController.value.duration.inSeconds - + 1) { + return; + } + setState(() => isAnimatingForward = true); + + Duration forward = Duration(seconds: position.value.inSeconds + 1); + await chewieController.videoPlayerController.seekTo(forward); + }, + )), + ], + )); + } + + Positioned playAndPause() { + return Positioned.fill( + child: Center( + child: InkWell( + onTap: _handlePlay, + child: Opacity( + opacity: isAnimatingBackward || isAnimatingForward ? 0 : 1, + child: PlayBtnAnimation( + alwaysAnimate: false, + isAnimating: isAnimating, + onEnd: () => setState( + () => isAnimating = false, + ), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.black.withValues(alpha: 0.4)), + child: Icon( + chewieController.isPlaying + ? CupertinoIcons.pause_fill + : CupertinoIcons.play_fill, + color: Colors.white, + size: 32, + ), + ), + ), + ), + ))); + } + + Positioned forwardBtn() { + return Positioned.fill( + child: Center( + child: Opacity( + opacity: isAnimatingForward ? 1 : 0, + child: PlayBtnAnimation( + alwaysAnimate: false, + isAnimating: isAnimatingForward, + onEnd: () => setState( + () => isAnimatingForward = false, + ), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.black.withValues(alpha: 0.4)), + child: const Icon( + Icons.forward_5_rounded, + color: Colors.white, + size: 32, + ), + ), + ), + ))); + } + + Positioned backwardBtn() { + return Positioned.fill( + child: Center( + child: Opacity( + opacity: isAnimatingBackward ? 1 : 0, + child: PlayBtnAnimation( + alwaysAnimate: false, + isAnimating: isAnimatingBackward, + onEnd: () => setState( + () => isAnimatingBackward = false, + ), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.black.withValues(alpha: 0.4)), + child: const Icon( + Icons.replay_5_rounded, + color: Colors.white, + size: 32, + ), + ), + ), + ))); + } + + Positioned forground() { + return Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.black, + Colors.black87, + Colors.black54, + Colors.black45, + Colors.black26, + Color.fromARGB(10, 0, 0, 0) + ])), + child: Row( + children: [ + // _buildPlayPause(), + _buildProgressIndicator(), + _buildFullScreenToggle(), + ], + ), + ), + ); + } + + Positioned speedPlayBackButtons() { + return Positioned( + top: 0, + right: 0, + left: 0, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black, + Colors.black87, + Colors.black54, + Colors.black45, + Colors.black26, + Color.fromARGB(10, 0, 0, 0) + ])), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SizedBox( + width: 120, + child: CustomDropdown( + closedHeaderPadding: EdgeInsets.zero, + itemsListPadding: EdgeInsets.zero, + items: const [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], + initialItem: 1, + listItemPadding: EdgeInsets.zero, + expandedHeaderPadding: EdgeInsets.zero, + + hideSelectedFieldWhenExpanded: false, + // overlayHeight: + // chewieController.isFullScreen ? null : 54 * 8, + decoration: CustomDropdownDecoration( + closedSuffixIcon: SizedBox(), + closedFillColor: Colors.transparent, + expandedBorderRadius: BorderRadius.circular(16)), + // hintText: "سرعت ویدیو", + listItemBuilder: (context, item, isSelected, onItemSelect) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 8), + child: Column( + children: [ + Text('x$item'), + if (item != 2) const Divider() + ], + )); + }, + headerBuilder: (context, selectedItem, enabled) => const Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Icon( + Icons.more_vert_rounded, + size: 32, + color: Colors.white, + ), + ], + ), + hintBuilder: (context, hint, enabled) => const SizedBox(), + onChanged: (value) async { + // isSpeedMenuOpen = false; + await chewieController.videoPlayerController + .setPlaybackSpeed(value!); + _startHideControlsTimer(); + }, + ), + ), + ], + ), + )); + } + + Widget _buildProgressIndicator() { + return Expanded( + child: ValueListenableBuilder( + valueListenable: position, + builder: (context, p, _) { + Duration duration = + chewieController.videoPlayerController.value.duration; + + return Column( + children: [ + SliderTheme( + data: SliderThemeData( + trackHeight: 2, + // thumbColor: Colors.transparent, + overlayShape: SliderComponentShape.noOverlay, + thumbShape: const RoundSliderThumbShape( + // elevation: 0, + // pressedElevation: 0, + enabledThumbRadius: 8)), + child: Slider( + min: 0, + max: duration.inMilliseconds.toDouble(), + value: p.inMilliseconds.toDouble(), + onChanged: (value) async { + await chewieController.pause(); + position.value = Duration(milliseconds: value.round()); + _startHideControlsTimer(); + }, + onChangeEnd: (value) async { + await chewieController + .seekTo(Duration(milliseconds: value.round())); + await chewieController.play(); + + setState(() {}); + }, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + DateTimeUtils.normalizeTimeDuration(p), + style: AppTextStyles.body4.copyWith(color: Colors.white), + ), + Text( + DateTimeUtils.normalizeTimeDuration(duration), + style: AppTextStyles.body4.copyWith(color: Colors.white), + ) + ], + ) + ], + ); + }, + ), + ); + } + + Widget _buildFullScreenToggle() { + return Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 8, 12), + child: InkWell( + onTap: () => setState(() { + chewieController.toggleFullScreen(); + _startHideControlsTimer(); // Restart the timer on tap + }), + child: Icon( + chewieController.isFullScreen + ? Icons.fullscreen_exit + : Icons.fullscreen, + color: Colors.white, + size: 30, + ), + ), + ); + } +} diff --git a/lib/ui/widgets/components/video/video_player.dart b/lib/ui/widgets/components/video/video_player.dart new file mode 100644 index 0000000..cece5d8 --- /dev/null +++ b/lib/ui/widgets/components/video/video_player.dart @@ -0,0 +1,88 @@ +// TutorialOverlay +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; +import 'package:chewie/chewie.dart'; + +class VideoPlayer extends ModalRoute { + final String initialUrl; + + VideoPlayer({required this.initialUrl}); + + @override + Duration get transitionDuration => const Duration(milliseconds: 500); + + @override + bool get opaque => false; + + @override + bool get barrierDismissible => true; + + @override + Color get barrierColor => Colors.black.withAlpha(210); + + @override + String get barrierLabel => 'Video'; + + @override + bool get maintainState => true; + + late final VideoPlayerController _controller = + VideoPlayerController.networkUrl(Uri.parse(initialUrl)); + late final ChewieController chewieController = ChewieController( + videoPlayerController: _controller, + autoPlay: true, + looping: true, + aspectRatio: 16 / 9); + + @override + void dispose() { + _controller.dispose(); + chewieController.dispose(); + super.dispose(); + } + + @override + Widget buildPage( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + return Material( + type: MaterialType.transparency, + child: SafeArea( + child: _buildOverlayContent(context), + ), + ); + } + + Widget _buildOverlayContent(BuildContext context) { + return Stack( + children: [ + Center( + child: Chewie( + controller: chewieController, + ), + ), + Positioned( + top: 16, + left: 16, + child: BackButton( + color: Theme.of(context).colorScheme.onSurface, + )) + ], + ); + } + + @override + Widget buildTransitions(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) { + // You can add your own animations for the overlay content + return FadeTransition( + opacity: animation, + child: ScaleTransition( + scale: animation, + child: child, + ), + ); + } +} diff --git a/lib/ui/widgets/components/video/video_player_widget.dart b/lib/ui/widgets/components/video/video_player_widget.dart new file mode 100644 index 0000000..cbaa1a9 --- /dev/null +++ b/lib/ui/widgets/components/video/video_player_widget.dart @@ -0,0 +1,35 @@ +import 'package:chewie/chewie.dart'; +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +class VideoPlayerWidget extends StatefulWidget { + final String url; + const VideoPlayerWidget({super.key, required this.url}); + + @override + State createState() => _VideoPlayerWidgetState(); +} + +class _VideoPlayerWidgetState extends State { + late final VideoPlayerController _controller = + VideoPlayerController.networkUrl(Uri.parse(widget.url)); + late final ChewieController chewieController = ChewieController( + videoPlayerController: _controller, + autoPlay: false, + looping: true, + aspectRatio: 16 / 9); + + @override + void dispose() { + _controller.dispose(); + chewieController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Chewie( + controller: chewieController, + ); + } +} diff --git a/lib/ui/widgets/components/video/video_thumbnail.dart b/lib/ui/widgets/components/video/video_thumbnail.dart new file mode 100644 index 0000000..86ffd26 --- /dev/null +++ b/lib/ui/widgets/components/video/video_thumbnail.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/ui/widgets/components/image/custome_image.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:get_thumbnail_video/index.dart'; +import 'package:get_thumbnail_video/video_thumbnail.dart'; + +class VideoThumbnailWidget extends StatefulWidget { + final String videoUrl; + + const VideoThumbnailWidget({super.key, required this.videoUrl}); + + @override + State createState() => _VideoThumbnailWidgetState(); +} + +class _VideoThumbnailWidgetState extends State { + XFile? _thumbnailFile; + + @override + void initState() { + super.initState(); + _generateThumbnail(); + } + + Future _generateThumbnail() async { + final thumbnailPath = await VideoThumbnail.thumbnailFile( + video: widget.videoUrl, + thumbnailPath: (await getTemporaryDirectory()).path, + imageFormat: ImageFormat.JPEG, + maxHeight: + 300, // specify the height of the thumbnail, let the width auto-scaled to keep the source aspect ratio + quality: 90, + maxWidth: 800, + ); + + setState(() { + _thumbnailFile = thumbnailPath; + }); + } + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: AspectRatio( + aspectRatio: 16 / 9, + child: _thumbnailFile != null + ? Stack( + children: [ + Center(child: CustomeImage(src: _thumbnailFile!.path)), + Positioned.fill( + child: Center( + child: Assets.icon.bold.play.svg( + width: 64, + color: Theme.of(context) + .colorScheme + .primary + .withAlpha(200)), + ), + ) + ], + ) + : DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Colors.white))), + ), + ); + } +} diff --git a/lib/ui/widgets/family_banner.dart b/lib/ui/widgets/family_banner.dart new file mode 100644 index 0000000..be464af --- /dev/null +++ b/lib/ui/widgets/family_banner.dart @@ -0,0 +1,57 @@ +// ignore_for_file: deprecated_member_use + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:hoshan/ui/screens/family/add_family.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; + +class FamilyBanner extends StatelessWidget { + const FamilyBanner({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: GestureDetector( + onTap: () { + final hasParent = UserInfoCubit.userInfoModel.parent != null; + + if (hasParent) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'شما عضو یک خانواده هستید و نمی‌توانید خانواده جدید اضافه کنید', + ), + ), + ); + return; + } + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AddFamily(), + ), + ); + }, + child: Container( + width: double.infinity, + height: 150, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: SvgPicture.asset('assets/icon/outline/family.svg')), + ), + ), + ); + } +} diff --git a/lib/ui/widgets/sections/empty/empty_screen.dart b/lib/ui/widgets/sections/empty/empty_screen.dart new file mode 100644 index 0000000..fdc2e4b --- /dev/null +++ b/lib/ui/widgets/sections/empty/empty_screen.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/ui/theme/text.dart'; + +class EmptyScreen extends StatelessWidget { + final AssetGenImage image; + final String title; + final double? width; + final double? height; + final double scale; + final TextStyle? style; + const EmptyScreen( + {super.key, + required this.image, + required this.title, + this.width, + this.height, + this.scale = 1, + this.style}); + + @override + Widget build(BuildContext context) { + return Transform.scale( + scale: scale, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: + SizedBox(width: 250, height: 250, child: image.image())), + Text( + title, + style: style ?? + AppTextStyles.headline5 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ), + const SizedBox( + height: 4, + ), + Assets.image.empty.emptyTextUnderline.svg(width: width) + ], + ), + ); + } +} diff --git a/lib/ui/widgets/sections/empty/empty_states.dart b/lib/ui/widgets/sections/empty/empty_states.dart new file mode 100644 index 0000000..bddd267 --- /dev/null +++ b/lib/ui/widgets/sections/empty/empty_states.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/data/model/empty_states_enum.dart'; +import 'package:hoshan/ui/widgets/sections/empty/empty_screen.dart'; + +class EmptyStates { + static Widget getEmptyState( + {required final EmptyStatesEnum status, + final double? width, + final double? height, + final double scale = 1, + final TextStyle? style, + final String? title}) { + EmptyStateModel data; + switch (status) { + case EmptyStatesEnum.inbox: + data = EmptyStateModel( + image: Assets.image.empty.inbox, + title: title ?? 'صندوق پیام خالی است'); + break; + case EmptyStatesEnum.amount: + data = EmptyStateModel( + image: Assets.image.empty.amount, + title: title ?? 'موجودی حساب کافی نیست'); + break; + case EmptyStatesEnum.server: + data = EmptyStateModel( + image: Assets.image.empty.server, title: title ?? 'سرور مشغول است'); + break; + case EmptyStatesEnum.connection: + data = EmptyStateModel( + image: Assets.image.empty.connection, + title: title ?? 'اینترنت قطع است'); + break; + case EmptyStatesEnum.archive: + data = EmptyStateModel( + image: Assets.image.empty.messages, + title: title ?? 'لیست آرشیو شده‌ها خالی است'); + break; + case EmptyStatesEnum.messages: + data = EmptyStateModel( + image: Assets.image.empty.messages, + title: title ?? 'لیست پیام‌ها خالی است'); + break; + case EmptyStatesEnum.assistant: + data = EmptyStateModel( + image: Assets.image.empty.assistant, + title: title ?? 'لیست دستیارها خالی است'); + break; + case EmptyStatesEnum.familyMembers: + data = EmptyStateModel( + image: + const AssetGenImage('assets/image/empty/empty state 1 1.png'), + title: title ?? 'هنوز عضوی به خانواده دعوت نشده است'); + break; + } + + return EmptyScreen( + width: width, + height: height, + image: data.image, + title: data.title, + scale: scale, + style: style, + ); + } +} diff --git a/lib/ui/widgets/sections/empty/not_found.dart b/lib/ui/widgets/sections/empty/not_found.dart new file mode 100644 index 0000000..7ce7f08 --- /dev/null +++ b/lib/ui/widgets/sections/empty/not_found.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class NotFoundPage extends StatelessWidget { + const NotFoundPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Scaffold( + body: Center( + child: Text( + '404 Not Found', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + )), + ); + } +} diff --git a/lib/ui/widgets/sections/header/home_appbar.dart b/lib/ui/widgets/sections/header/home_appbar.dart new file mode 100644 index 0000000..0a46724 --- /dev/null +++ b/lib/ui/widgets/sections/header/home_appbar.dart @@ -0,0 +1,287 @@ +// ignore_for_file: deprecated_member_use_from_same_package, use_build_context_synchronously + +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/gen/assets.gen.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/core/services/api/dio_service.dart'; +import 'package:hoshan/core/utils/date_time.dart'; +import 'package:hoshan/data/model/empty_states_enum.dart'; +import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; +import 'package:hoshan/ui/widgets/components/button/circle_icon_btn.dart'; +import 'package:hoshan/ui/widgets/sections/empty/empty_states.dart'; +import 'package:persian_number_utility/persian_number_utility.dart'; +import 'package:flutter/services.dart'; + +class HomeAppbar extends StatelessWidget implements PreferredSizeWidget { + static const platform = MethodChannel('webview_channel'); + + final BuildContext context; + + const HomeAppbar(this.context, {super.key}); + + Future _openWebView() async { + try { + await platform.invokeMethod('openWebView'); + } on PlatformException catch (e) { + debugPrint("Failed to open WebView: '${e.message}'."); + } + } + + @override + Widget build(BuildContext context) { + return AppBar( + toolbarHeight: preferredSize.height, + shadowColor: AppColors.black.defaultShade.withValues(alpha: 0.15), + elevation: 4, + title: Flex( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + direction: Axis.horizontal, + children: [ + Flexible( + flex: 1, + child: Row( + children: [ + // CircleIconBtn( + // size: Responsive(context).isMobile() ? 32 : 46, + // iconPadding: Responsive(context).isMobile() + // ? null + // : const EdgeInsets.all(8), + // icon: Assets.icon.bold.setting, + // color: context.read().isDark() + // ? AppColors.black[900] + // : AppColors.primaryColor[50], + // iconColor: Theme.of(context).colorScheme.primary, + // onTap: () { + // context.go(Routes.setting); + // }, + // ), + // SizedBox( + // width: Responsive(context).isMobile() ? 4 : 8, + // ), + PopupMenuButton( + tooltip: '', + splashRadius: 0, + onOpened: () async { + try { + DioService() + .sendRequest() + .put(DioService.readAllNotifications); + final notifs = + UserInfoCubit.userInfoModel.notifications!.map( + (e) { + e.seen = true; + return e; + }, + ).toList(); + UserInfoCubit.userInfoModel.notifications = notifs; + context + .read() + .changeUser(UserInfoCubit.userInfoModel); + } on DioException catch (e) { + if (kDebugMode) { + print("Dio Error is: $e"); + } + } + }, + menuPadding: const EdgeInsets.all(16), + offset: const Offset(-46, 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + position: PopupMenuPosition.under, + constraints: BoxConstraints( + maxWidth: MediaQuery.sizeOf(context).width * 0.7, + maxHeight: MediaQuery.sizeOf(context).height * 0.6), + itemBuilder: (context) { + if (UserInfoCubit.userInfoModel.notifications != null && + UserInfoCubit.userInfoModel.notifications!.isNotEmpty) { + return List.generate( + (UserInfoCubit.userInfoModel.notifications?.length ?? + 0), + (index) { + final notif = + UserInfoCubit.userInfoModel.notifications![index]; + return PopupMenuItem( + child: Directionality( + textDirection: TextDirection.rtl, + child: Column( + children: [ + ListTile( + title: Text( + notif.title ?? '', + style: AppTextStyles.body4.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context) + .colorScheme + .onSurface), + ), + subtitle: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + notif.message ?? '', + style: AppTextStyles.body4.copyWith( + color: AppColors.gray[context + .read() + .isDark() + ? 600 + : 900]), + ), + const SizedBox( + height: 4, + ), + Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + Text( + '${DateTimeUtils.convertToSentTime(notif.createdAt ?? '')} - ${DateTimeUtils.convertStringIsoToDate(notif.createdAt ?? '').toPersianDateStr()} ', + style: AppTextStyles.body6.copyWith( + color: Theme.of(context) + .colorScheme + .primary), + ) + ], + ) + ], + ), + ), + if (index != + UserInfoCubit.userInfoModel.notifications! + .length - + 1) + const Divider() + ], + ), + )); + }, + ); + } + return [ + PopupMenuItem( + child: EmptyStates.getEmptyState( + status: EmptyStatesEnum.inbox, + title: 'صندوق رویدادها خالی است', + style: AppTextStyles.body4 + .copyWith(fontWeight: FontWeight.bold))) + ]; + }, + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: CircleIconBtn( + size: Responsive(context).isMobile() ? 32 : 46, + iconPadding: Responsive(context).isMobile() + ? null + : const EdgeInsets.all(8), + icon: Assets.icon.outline.notificationBing, + color: context.read().isDark() + ? AppColors.black[900] + : AppColors.primaryColor[50], + iconColor: Theme.of(context).colorScheme.primary, + )), + if (UserInfoCubit.userInfoModel.notifications != null && + UserInfoCubit.userInfoModel.notifications!.isNotEmpty) + Positioned( + top: 0, + right: 0, + child: BlocBuilder( + builder: (context, state) { + try { + final notSeen = + UserInfoCubit.userInfoModel.notifications! + .where( + (e) => e.seen == false, + ) + .toList(); + if (notSeen.isEmpty) { + return const SizedBox.shrink(); + } + + return Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.red.defaultShade), + child: Padding( + padding: const EdgeInsets.all(6), + child: Text( + notSeen.length.toString(), + style: AppTextStyles.body6.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold), + ), + ), + ) + .animate( + autoPlay: true, + onPlay: (controller) => + controller.repeat(reverse: true), + ) + .scale( + begin: const Offset(1, 1), + end: const Offset(1.2, 1.2), + duration: 600.ms, + curve: Curves.easeInOut, + ); + } catch (e) { + return const SizedBox.shrink(); + } + })) + ], + ), + ), + ], + ), + ), + Flexible( + flex: 1, + child: GestureDetector( + onTap: () => context.go(Routes.myAccount), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + CircleIconBtn( + size: Responsive(context).isMobile() ? 32 : 46, + iconPadding: Responsive(context).isMobile() + ? null + : const EdgeInsets.all(8), + icon: Assets.icon.outline.coin, + color: context.read().isDark() + ? AppColors.black[900] + : AppColors.secondryColor[50], + iconColor: Theme.of(context).colorScheme.secondary, + ) + .animate( + autoPlay: true, + onPlay: (controller) => + controller.repeat(reverse: true), + ) + .moveY( + begin: 0, + end: -15, + duration: 800.ms, + curve: Curves.easeInBack, + delay: 30.seconds) + ], + ), + ), + ), + ], + ), + ); + } + + @override + Size get preferredSize => Size.fromHeight( + kToolbarHeight + (Responsive(context).isMobile() ? 12 : 32)); +} diff --git a/lib/ui/widgets/sections/header/primary_appbar.dart b/lib/ui/widgets/sections/header/primary_appbar.dart new file mode 100644 index 0000000..1649499 --- /dev/null +++ b/lib/ui/widgets/sections/header/primary_appbar.dart @@ -0,0 +1,54 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:hoshan/ui/screens/main/home_page.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; + +class PrimaryAppbar extends StatelessWidget implements PreferredSizeWidget { + final BuildContext context; + + final List? actions; + final String? titleText; + final Function()? onBack; + const PrimaryAppbar(this.context, + {super.key, this.actions, this.titleText, this.onBack}); + + @override + Widget build(BuildContext context) { + return AppBar( + title: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (titleText != null) + Text( + titleText!, + style: AppTextStyles.body3 + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ), + ], + ), + actions: actions, + automaticallyImplyLeading: false, + toolbarHeight: preferredSize.height, + leadingWidth: 100, + leading: GestureDetector( + onTap: () { + onBack?.call(); + screenIndex.value = 0; + }, + child: Container( + color: Theme.of(context).appBarTheme.backgroundColor, + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: const Icon(CupertinoIcons.back)), + ), + shadowColor: AppColors.black.defaultShade.withValues(alpha: 0.15), + elevation: 4, + ); + } + + @override + Size get preferredSize => Size.fromHeight( + kToolbarHeight + (Responsive(context).isMobile() ? 12 : 32)); +} diff --git a/lib/ui/widgets/sections/header/reversible_appbar.dart b/lib/ui/widgets/sections/header/reversible_appbar.dart new file mode 100644 index 0000000..97b0c32 --- /dev/null +++ b/lib/ui/widgets/sections/header/reversible_appbar.dart @@ -0,0 +1,60 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hoshan/core/routes/route_generator.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/responsive.dart'; +import 'package:hoshan/ui/theme/text.dart'; + +class ReversibleAppbar extends StatelessWidget implements PreferredSizeWidget { + final Widget? title; + final String? titleText; + final BuildContext context; + + const ReversibleAppbar(this.context, {super.key, this.title, this.titleText}); + + @override + Widget build(BuildContext context) { + return AppBar( + title: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + title ?? + Text( + titleText ?? '', + style: Responsive(context).isMobile() + ? AppTextStyles.body3 + : AppTextStyles.body1, + ), + ], + ), + toolbarHeight: preferredSize.height, + automaticallyImplyLeading: false, + leadingWidth: 100, + leading: GestureDetector( + onTap: () { + try { + context.pop(); + } catch (e) { + context.go(Routes.main); + } + }, + child: Container( + color: Theme.of(context).appBarTheme.backgroundColor, + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Icon( + CupertinoIcons.back, + size: Responsive(context).isMobile() ? null : 32, + ), + ), + ), + shadowColor: AppColors.black.defaultShade.withValues(alpha: 0.15), + elevation: 4, + ); + } + + @override + Size get preferredSize => Size.fromHeight( + kToolbarHeight + (Responsive(context).isMobile() ? 0 : 32)); +} diff --git a/lib/ui/widgets/sections/header/sticky_header.dart b/lib/ui/widgets/sections/header/sticky_header.dart new file mode 100644 index 0000000..88b9e94 --- /dev/null +++ b/lib/ui/widgets/sections/header/sticky_header.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class StickyHeader extends SliverPersistentHeaderDelegate { + final Widget child; + final double maxExtentSize; + final double minExtentSize; + + StickyHeader( + {required this.child, + required this.maxExtentSize, + required this.minExtentSize}); + + @override + Widget build( + BuildContext context, double shrinkOffset, bool overlapsContent) { + return child; + } + + @override + double get maxExtent => maxExtentSize; // Height of the sticky widget + @override + double get minExtent => minExtentSize; // Minimum height when collapsed + @override + bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { + return true; + } +} diff --git a/lib/ui/widgets/sections/loading/chat_screen_placeholder.dart b/lib/ui/widgets/sections/loading/chat_screen_placeholder.dart new file mode 100644 index 0000000..567068d --- /dev/null +++ b/lib/ui/widgets/sections/loading/chat_screen_placeholder.dart @@ -0,0 +1,45 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; + +class ChatScreenPlaceholder extends StatelessWidget { + const ChatScreenPlaceholder({super.key}); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Column( + children: [ + const SizedBox( + height: 46, + ), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: 10, + itemBuilder: (context, index) => DefaultPlaceHolder( + child: Padding( + padding: EdgeInsets.fromLTRB(index % 2 == 0 ? 16 : 32, 0, + index % 2 == 0 ? 32 : 16, 16), + child: Container( + width: MediaQuery.sizeOf(context).width, + height: Random().nextInt(57) + 64, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16).copyWith( + topRight: + Radius.circular(index % 2 == 0 ? 10 : 0), + bottomLeft: + Radius.circular(index % 2 == 0 ? 0 : 10)), + ), + padding: const EdgeInsets.all(16), + ), + ), + )), + ], + ), + ); + } +} diff --git a/lib/ui/widgets/sections/loading/default_placeholder.dart b/lib/ui/widgets/sections/loading/default_placeholder.dart new file mode 100644 index 0000000..9cbfc2d --- /dev/null +++ b/lib/ui/widgets/sections/loading/default_placeholder.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:shimmer/shimmer.dart'; + +class DefaultPlaceHolder extends StatelessWidget { + final Widget child; + final bool enabled; + final double? width; + final double? height; + const DefaultPlaceHolder( + {super.key, + required this.child, + this.enabled = true, + this.width, + this.height}); + + @override + Widget build(BuildContext context) { + final isDark = context.read().state == ThemeMode.dark; + return enabled + ? IgnorePointer( + ignoring: true, + child: SizedBox( + width: width, + height: height, + child: Shimmer.fromColors( + baseColor: AppColors.gray[isDark ? 800 : 400], + highlightColor: AppColors.gray[isDark ? 900 : 600], + child: child), + ), + ) + : child; + } +} diff --git a/lib/ui/widgets/sections/loading/listview_placeholder.dart b/lib/ui/widgets/sections/loading/listview_placeholder.dart new file mode 100644 index 0000000..71c5d96 --- /dev/null +++ b/lib/ui/widgets/sections/loading/listview_placeholder.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hoshan/ui/theme/colors.dart'; +import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; +import 'package:shimmer/shimmer.dart'; + +class ListviewPlaceholder extends StatelessWidget { + final int count; + final double? itemWidth; + final double? itemHeight; + final Widget child; + final Axis scrollDirection; + const ListviewPlaceholder( + {super.key, + this.count = 10, + this.itemWidth, + this.itemHeight, + required this.child, + this.scrollDirection = Axis.vertical}); + + @override + Widget build(BuildContext context) { + final isDark = context.read().state == ThemeMode.dark; + + return ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: count, + padding: const EdgeInsets.symmetric(vertical: 8), + scrollDirection: scrollDirection, + itemBuilder: (context, index) { + return SizedBox( + width: itemWidth, + height: itemHeight, + child: Shimmer.fromColors( + baseColor: AppColors.gray[isDark ? 800 : 400], + highlightColor: AppColors.gray[isDark ? 900 : 600], + child: child), + ); + }, + ); + } +} diff --git a/lib/ui/widgets/sections/loading/random_container.dart b/lib/ui/widgets/sections/loading/random_container.dart new file mode 100644 index 0000000..e114d19 --- /dev/null +++ b/lib/ui/widgets/sections/loading/random_container.dart @@ -0,0 +1,47 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; + +class RandomContainer extends StatelessWidget { + final bool isUser; + const RandomContainer({super.key, required this.isUser}); + + @override + Widget build(BuildContext context) { + double maxWidth = MediaQuery.of(context).size.width * 0.8; + double randomWidth = Random().nextDouble() * (maxWidth - 120) + 120; + double oneLineHeight = 28.0; + double threeLinesHeight = oneLineHeight * 3; + double randomHeight = + Random().nextDouble() * (threeLinesHeight - oneLineHeight) + + oneLineHeight; + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + alignment: isUser ? Alignment.centerRight : Alignment.centerLeft, + child: DefaultPlaceHolder( + width: randomWidth, + child: Column( + children: [ + Container( + height: randomHeight, + padding: const EdgeInsets.all(8.0), + constraints: BoxConstraints( + maxWidth: MediaQuery.sizeOf(context).width * 0.8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16).copyWith( + bottomLeft: isUser + ? const Radius.circular(16) + : const Radius.circular(0), + bottomRight: isUser + ? const Radius.circular(0) + : const Radius.circular(16)), + color: Colors.white, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui/widgets/sections/loading/tools_placeholder.dart b/lib/ui/widgets/sections/loading/tools_placeholder.dart new file mode 100644 index 0000000..8a39a3a --- /dev/null +++ b/lib/ui/widgets/sections/loading/tools_placeholder.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; + +class ToolsPlaceholder extends StatelessWidget { + const ToolsPlaceholder({super.key}); + + @override + Widget build(BuildContext context) { + return GridView.builder( + itemCount: 10, + shrinkWrap: true, + padding: const EdgeInsets.all(16), + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, // number of items in each row + mainAxisSpacing: 16.0, // spacing between rows + crossAxisSpacing: 16.0, // spacing between columns + ), + itemBuilder: (context, index) { + return DefaultPlaceHolder( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + )), + ); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..31fd34f --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,2210 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 + url: "https://pub.dev" + source: hosted + version: "80.0.0" + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "7fd72d77a7487c26faab1d274af23fb008763ddc10800261abbfb2c067f183d5" + url: "https://pub.dev" + source: hosted + version: "1.3.53" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" + url: "https://pub.dev" + source: hosted + version: "7.3.0" + animated_custom_dropdown: + dependency: "direct main" + description: + name: animated_custom_dropdown + sha256: "5a72dc209041bb53f6c7164bc2e366552d5197cdb032b1c9b2c36e3013024486" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + another_flushbar: + dependency: "direct main" + description: + name: another_flushbar + sha256: "19bf9520230ec40b300aaf9dd2a8fefcb277b25ecd1c4838f530566965befc2a" + url: "https://pub.dev" + source: hosted + version: "1.12.30" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba" + url: "https://pub.dev" + source: hosted + version: "6.4.0" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + archive: + dependency: transitive + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + url: "https://pub.dev" + source: hosted + version: "2.12.0" + audioplayers: + dependency: "direct main" + description: + name: audioplayers + sha256: a5341380a4f1d3a10a4edde5bb75de5127fe31e0faa8c4d860e64d2f91ad84c7 + url: "https://pub.dev" + source: hosted + version: "6.4.0" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: f8c90823a45b475d2c129f85bbda9c029c8d4450b172f62e066564c6e170f69a + url: "https://pub.dev" + source: hosted + version: "5.2.0" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: "405cdbd53ebdb4623f1c5af69f275dad4f930ce895512d5261c07cd95d23e778" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: "7e0d081a6a527c53aef9539691258a08ff69a7dc15ef6335fbea1b4b03ebbef0" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "77e5fa20fb4a64709158391c75c1cca69a481d35dc879b519e350a05ff520373" + url: "https://pub.dev" + source: hosted + version: "7.1.0" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: bd99d8821114747682a2be0adcdb70233d4697af989b549d3a20a0f49f6c9b13 + url: "https://pub.dev" + source: hosted + version: "5.1.0" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: "871d3831c25cd2408ddc552600fd4b32fba675943e319a41284704ee038ad563" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + background_downloader: + dependency: "direct main" + description: + name: background_downloader + sha256: "2d4c2b7438e7643585880f9cc00ace16a52d778088751f1bfbf714627b315462" + url: "https://pub.dev" + source: hosted + version: "9.2.0" + before_after: + dependency: "direct main" + description: + name: before_after + sha256: "71a8b3f1d593eb62f6998b4c0ebf6e8e77446029510e4a608649f3b0f7fc1524" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189" + url: "https://pub.dev" + source: hosted + version: "9.0.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + url: "https://pub.dev" + source: hosted + version: "4.0.4" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 + url: "https://pub.dev" + source: hosted + version: "2.4.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" + url: "https://pub.dev" + source: hosted + version: "2.4.15" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + url: "https://pub.dev" + source: hosted + version: "8.0.0" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 + url: "https://pub.dev" + source: hosted + version: "8.9.5" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + carousel_slider: + dependency: "direct main" + description: + name: carousel_slider + sha256: "7b006ec356205054af5beaef62e2221160ea36b90fb70a35e4deacd49d0349ae" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + change_app_package_name: + dependency: "direct main" + description: + name: change_app_package_name + sha256: "8e43b754fe960426904d77ed4c62fa8c9834deaf6e293ae40963fa447482c4c5" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + chewie: + dependency: "direct main" + description: + name: chewie + sha256: "0bf6f7692cb65f7b8f59a2a17025b9cbe8f75ab4251e66161a4fc86162475fb6" + url: "https://pub.dev" + source: hosted + version: "1.11.0" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + url: "https://pub.dev" + source: hosted + version: "4.10.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + color: + dependency: transitive + description: + name: color + sha256: ddcdf1b3badd7008233f5acffaf20ca9f5dc2cd0172b75f68f24526a5f5725cb + url: "https://pub.dev" + source: hosted + version: "3.0.0" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + cross_file: + dependency: "direct main" + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + dartx: + dependency: transitive + description: + name: dartx + sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + dio: + dependency: "direct main" + description: + name: dio + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + url: "https://pub.dev" + source: hosted + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + easy_debounce: + dependency: "direct main" + description: + name: easy_debounce + sha256: f082609cfb8f37defb9e37fc28bc978c6712dedf08d4c5a26f820fa10165a236 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" + excel: + dependency: "direct main" + description: + name: excel + sha256: "1a15327dcad260d5db21d1f6e04f04838109b39a2f6a84ea486ceda36e468780" + url: "https://pub.dev" + source: hosted + version: "4.0.6" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "127d84b954527b2a59208c5cba556d8fb9078538f41ec869a56651f72f212a4b" + url: "https://pub.dev" + source: hosted + version: "9.2.0" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" + url: "https://pub.dev" + source: hosted + version: "0.9.3+4" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + sha256: "91587615d7d9165c65a030426e3cf40bbec37c486f52ff654af17aba5be3d208" + url: "https://pub.dev" + source: hosted + version: "5.5.1" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + sha256: "1dcf1dbdd90fe97fa37ab3631b561bf584adb88f6be0b0dd915fff799ad53192" + url: "https://pub.dev" + source: hosted + version: "7.6.1" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + sha256: "3774cb13547e28b180fed2a5e696b4b36f97f4b1fadc7b04a0200e5009344d98" + url: "https://pub.dev" + source: hosted + version: "5.14.1" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: f4d8f49574a4e396f34567f3eec4d38ab9c3910818dec22ca42b2a467c685d8b + url: "https://pub.dev" + source: hosted + version: "3.12.1" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf + url: "https://pub.dev" + source: hosted + version: "5.4.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: faa5a76f6380a9b90b53bc3bdcb85bc7926a382e0709b9b5edac9f7746651493 + url: "https://pub.dev" + source: hosted + version: "2.21.1" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + sha256: "5fc345c6341f9dc69fd0ffcbf508c784fd6d1b9e9f249587f30434dd8b6aa281" + url: "https://pub.dev" + source: hosted + version: "15.2.4" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: a935924cf40925985c8049df4968b1dde5c704f570f3ce380b31d3de6990dd94 + url: "https://pub.dev" + source: hosted + version: "4.6.4" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: fafebf6a1921931334f3f10edb5037a5712288efdd022881e2d093e5654a2fd4 + url: "https://pub.dev" + source: hosted + version: "3.10.4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08" + url: "https://pub.dev" + source: hosted + version: "0.69.2" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_animate: + dependency: "direct main" + description: + name: flutter_animate + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" + url: "https://pub.dev" + source: hosted + version: "4.5.2" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: "1046d719fbdf230330d3443187cc33cc11963d15c9089f6cc56faa42a4c5f0cc" + url: "https://pub.dev" + source: hosted + version: "9.1.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_downloader: + dependency: "direct main" + description: + name: flutter_downloader + sha256: "93a9ddbd561f8a3f5483b4189453fba145a0a1014a88143c96a966296b78a118" + url: "https://pub.dev" + source: hosted + version: "1.12.0" + flutter_gen_core: + dependency: transitive + description: + name: flutter_gen_core + sha256: "3eaa2d3d8be58267ac4cd5e215ac965dd23cae0410dc073de2e82e227be32bfc" + url: "https://pub.dev" + source: hosted + version: "5.10.0" + flutter_gen_runner: + dependency: "direct dev" + description: + name: flutter_gen_runner + sha256: e74b4ead01df3e8f02e73a26ca856759dbbe8cb3fd60941ba9f4005cd0cd19c9 + url: "https://pub.dev" + source: hosted + version: "5.10.0" + flutter_highlight: + dependency: "direct main" + description: + name: flutter_highlight + sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + flutter_html: + dependency: "direct main" + description: + name: flutter_html + sha256: "38a2fd702ffdf3243fb7441ab58aa1bc7e6922d95a50db76534de8260638558d" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_image_compress: + dependency: "direct main" + description: + name: flutter_image_compress + sha256: "51d23be39efc2185e72e290042a0da41aed70b14ef97db362a6b5368d0523b27" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + flutter_image_compress_common: + dependency: transitive + description: + name: flutter_image_compress_common + sha256: c5c5d50c15e97dd7dc72ff96bd7077b9f791932f2076c5c5b6c43f2c88607bfb + url: "https://pub.dev" + source: hosted + version: "1.0.6" + flutter_image_compress_macos: + dependency: transitive + description: + name: flutter_image_compress_macos + sha256: "20019719b71b743aba0ef874ed29c50747461e5e8438980dfa5c2031898f7337" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + flutter_image_compress_ohos: + dependency: transitive + description: + name: flutter_image_compress_ohos + sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51 + url: "https://pub.dev" + source: hosted + version: "0.0.3" + flutter_image_compress_platform_interface: + dependency: transitive + description: + name: flutter_image_compress_platform_interface + sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + flutter_image_compress_web: + dependency: transitive + description: + name: flutter_image_compress_web + sha256: b9b141ac7c686a2ce7bb9a98176321e1182c9074650e47bb140741a44b6f5a96 + url: "https://pub.dev" + source: hosted + version: "0.1.5" + flutter_inappwebview: + dependency: "direct main" + description: + name: flutter_inappwebview + sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + flutter_inappwebview_android: + dependency: transitive + description: + name: flutter_inappwebview_android + sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + flutter_inappwebview_ios: + dependency: transitive + description: + name: flutter_inappwebview_ios + sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_macos: + dependency: transitive + description: + name: flutter_inappwebview_macos + sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_platform_interface: + dependency: transitive + description: + name: flutter_inappwebview_platform_interface + sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 + url: "https://pub.dev" + source: hosted + version: "1.3.0+1" + flutter_inappwebview_web: + dependency: transitive + description: + name: flutter_inappwebview_web + sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_windows: + dependency: transitive + description: + name: flutter_inappwebview_windows + sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + flutter_launcher_icons: + dependency: "direct main" + description: + name: flutter_launcher_icons + sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c + url: "https://pub.dev" + source: hosted + version: "0.14.3" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_math_fork: + dependency: "direct main" + description: + name: flutter_math_fork + sha256: "6d5f2f1aa57ae539ffb0a04bb39d2da67af74601d685a161aff7ce5bda5fa407" + url: "https://pub.dev" + source: hosted + version: "0.7.4" + flutter_media_downloader: + dependency: "direct main" + description: + name: flutter_media_downloader + sha256: "7a7a3a7adebbb6b35da57d55cd1d4a8d8c1edcf3be6ebef71cb90167256787cd" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3" + url: "https://pub.dev" + source: hosted + version: "2.0.27" + flutter_rating_bar: + dependency: "direct main" + description: + name: flutter_rating_bar + sha256: d2af03469eac832c591a1eba47c91ecc871fe5708e69967073c043b2d775ed93 + url: "https://pub.dev" + source: hosted + version: "4.0.1" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + url: "https://pub.dev" + source: hosted + version: "0.1.3" + flutter_sound: + dependency: "direct main" + description: + name: flutter_sound + sha256: "8948391be95f0e121a9f781e1edf5f783f8d1f2f4cc3ce363b342374cde6665f" + url: "https://pub.dev" + source: hosted + version: "9.26.0" + flutter_sound_platform_interface: + dependency: transitive + description: + name: flutter_sound_platform_interface + sha256: e4d01beb992dcf37e5cff171053dca81c472dcc662421db4fa55ff43fe94703f + url: "https://pub.dev" + source: hosted + version: "9.26.0" + flutter_sound_web: + dependency: transitive + description: + name: flutter_sound_web + sha256: "7d3c98889e911184db72128284b5c6b114ea92f1a4bc2142d3301388ab42ed62" + url: "https://pub.dev" + source: hosted + version: "9.26.0" + flutter_spinkit: + dependency: "direct main" + description: + name: flutter_spinkit + sha256: d2696eed13732831414595b98863260e33e8882fc069ee80ec35d4ac9ddb0472 + url: "https://pub.dev" + source: hosted + version: "5.2.1" + flutter_staggered_grid_view: + dependency: "direct main" + description: + name: flutter_staggered_grid_view + sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b + url: "https://pub.dev" + source: hosted + version: "2.0.17" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + get_thumbnail_video: + dependency: "direct main" + description: + name: get_thumbnail_video + sha256: ff61495b42051765d2a9e93bd14dac7ede5853033837bde71c27575a192c53fc + url: "https://pub.dev" + source: hosted + version: "0.7.3" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 + url: "https://pub.dev" + source: hosted + version: "14.8.1" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "55580f436822d64c8ff9a77e37d61f5fb1e6c7ec9d632a43ee324e2a05c3c6c9" + url: "https://pub.dev" + source: hosted + version: "0.3.3" + google_mobile_ads: + dependency: "direct main" + description: + name: google_mobile_ads + sha256: d2ef5ec1e1f31137fc241bdeab3037c31062d387dd221fd884fb1160444c788b + url: "https://pub.dev" + source: hosted + version: "4.0.0" + google_sign_in: + dependency: "direct main" + description: + name: google_sign_in + sha256: d0a2c3bcb06e607bb11e4daca48bd4b6120f0bbc4015ccebbe757d24ea60ed2a + url: "https://pub.dev" + source: hosted + version: "6.3.0" + google_sign_in_android: + dependency: transitive + description: + name: google_sign_in_android + sha256: "4e52c64366bdb3fe758f683b088ee514cc7a95e69c52b5ee9fc5919e1683d21b" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + google_sign_in_ios: + dependency: transitive + description: + name: google_sign_in_ios + sha256: "29cd125f58f50ceb40e8253d3c0209e321eee3e5df16cd6d262495f7cad6a2bd" + url: "https://pub.dev" + source: hosted + version: "5.8.1" + google_sign_in_platform_interface: + dependency: transitive + description: + name: google_sign_in_platform_interface + sha256: "5f6f79cf139c197261adb6ac024577518ae48fdff8e53205c5373b5f6430a8aa" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + google_sign_in_web: + dependency: transitive + description: + name: google_sign_in_web + sha256: "460547beb4962b7623ac0fb8122d6b8268c951cf0b646dd150d60498430e4ded" + url: "https://pub.dev" + source: hosted + version: "0.12.4+4" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" + hashcodes: + dependency: transitive + description: + name: hashcodes + sha256: "80f9410a5b3c8e110c4b7604546034749259f5d6dcca63e0d3c17c9258f1a651" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + highlight: + dependency: transitive + description: + name: highlight + sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + html: + dependency: transitive + description: + name: html + sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" + url: "https://pub.dev" + source: hosted + version: "0.15.5" + http: + dependency: transitive + description: + name: http + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + url: "https://pub.dev" + source: hosted + version: "1.3.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d + url: "https://pub.dev" + source: hosted + version: "4.3.0" + image_cropper: + dependency: "direct main" + description: + name: image_cropper + sha256: "0c6ea3f96ccdcbe855fc86e9de582fdd6a94d578be8d817a05d9ecef9f1a258b" + url: "https://pub.dev" + source: hosted + version: "9.0.0" + image_cropper_for_web: + dependency: transitive + description: + name: image_cropper_for_web + sha256: "34256c8fb7fcb233251787c876bb37271744459b593a948a2db73caa323034d0" + url: "https://pub.dev" + source: hosted + version: "6.0.2" + image_cropper_platform_interface: + dependency: transitive + description: + name: image_cropper_platform_interface + sha256: e8e9d2ca36360387aee39295ce49029362ae4df3071f23e8e71f2b81e40b7531 + url: "https://pub.dev" + source: hosted + version: "7.0.0" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: "direct main" + description: + name: image_picker_android + sha256: "8bd392ba8b0c8957a157ae0dc9fcf48c58e6c20908d5880aea1d79734df090e9" + url: "https://pub.dev" + source: hosted + version: "0.8.12+22" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" + url: "https://pub.dev" + source: hosted + version: "0.8.12+2" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" + url: "https://pub.dev" + source: hosted + version: "2.10.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_size_getter: + dependency: transitive + description: + name: image_size_getter + sha256: "9a299e3af2ebbcfd1baf21456c3c884037ff524316c97d8e56035ea8fdf35653" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + list_counter: + dependency: transitive + description: + name: list_counter + sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + logger: + dependency: transitive + description: + name: logger + sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 + url: "https://pub.dev" + source: hosted + version: "2.5.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + markdown: + dependency: transitive + description: + name: markdown + sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" + url: "https://pub.dev" + source: hosted + version: "7.3.0" + markdown_widget: + dependency: "direct main" + description: + name: markdown_widget + sha256: "216dced98962d7699a265344624bc280489d739654585ee881c95563a3252fac" + url: "https://pub.dev" + source: hosted + version: "2.3.2+6" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + open_filex: + dependency: "direct main" + description: + name: open_filex + sha256: "9976da61b6a72302cf3b1efbce259200cd40232643a467aac7370addf94d6900" + url: "https://pub.dev" + source: hosted + version: "4.7.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" + url: "https://pub.dev" + source: hosted + version: "8.3.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" + url: "https://pub.dev" + source: hosted + version: "2.2.16" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + percent_indicator: + dependency: "direct main" + description: + name: percent_indicator + sha256: "0d77d5c6fa9b7f60202cedf748b568ba9ba38d3f30405d6ceae4da76f5185462" + url: "https://pub.dev" + source: hosted + version: "4.2.4" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" + url: "https://pub.dev" + source: hosted + version: "11.4.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + url: "https://pub.dev" + source: hosted + version: "12.1.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f84a188e79a35c687c132a0a0556c254747a08561e99ab933f12f6ca71ef3c98 + url: "https://pub.dev" + source: hosted + version: "9.4.6" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + persian_number_utility: + dependency: "direct main" + description: + name: persian_number_utility + sha256: d171732c8d6bd1b710de3d4b4f11c8d326e20e9c462968df78c5358dcbc189b4 + url: "https://pub.dev" + source: hosted + version: "1.1.4" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + pinput: + dependency: "direct main" + description: + name: pinput + sha256: "8a73be426a91fefec90a7f130763ca39772d547e92f19a827cf4aa02e323d35a" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + popover: + dependency: "direct main" + description: + name: popover + sha256: "0606f3e10f92fc0459f5c52fd917738c29e7552323b28694d50c2d3312d0e1a2" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + pretty_dio_logger: + dependency: "direct main" + description: + name: pretty_dio_logger + sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + rename_app: + dependency: "direct main" + description: + name: rename_app + sha256: "7cb304cea0b3cbcc332eab80b21df3c168f25ee47e59e22f88c52476e8ae3e97" + url: "https://pub.dev" + source: hosted + version: "1.6.2" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + scroll_to_index: + dependency: transitive + description: + name: scroll_to_index + sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176 + url: "https://pub.dev" + source: hosted + version: "3.0.1" + sentry: + dependency: transitive + description: + name: sentry + sha256: "077b03f9ee44cfb1eaadbf8af58255e670de62b3f240ca154ce96a5591dc3885" + url: "https://pub.dev" + source: hosted + version: "8.14.1" + sentry_flutter: + dependency: "direct main" + description: + name: sentry_flutter + sha256: a348e2a365a8ad7682dd09db54f50f19f1c87180b8278f088bc393c511aea5e0 + url: "https://pub.dev" + source: hosted + version: "8.14.1" + shamsi_date: + dependency: "direct main" + description: + name: shamsi_date + sha256: "4614789ed11bfffe5ba0aa157a20f2857ab6328528401766e0d924e453c866bd" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da + url: "https://pub.dev" + source: hosted + version: "10.1.4" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b + url: "https://pub.dev" + source: hosted + version: "5.0.2" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" + url: "https://pub.dev" + source: hosted + version: "2.5.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad" + url: "https://pub.dev" + source: hosted + version: "2.4.8" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + smart_auth: + dependency: "direct main" + description: + name: smart_auth + sha256: a536423c50d71e9a311d16027346634d0deadd07dafc5b5606b46719cf6fb2b6 + url: "https://pub.dev" + source: hosted + version: "3.2.0" + smooth_page_indicator: + dependency: "direct main" + description: + name: smooth_page_indicator + sha256: b21ebb8bc39cf72d11c7cfd809162a48c3800668ced1c9da3aade13a32cf6c1c + url: "https://pub.dev" + source: hosted + version: "1.2.1" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" + url: "https://pub.dev" + source: hosted + version: "2.5.4+6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c" + url: "https://pub.dev" + source: hosted + version: "2.4.1+1" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + string_validator: + dependency: "direct main" + description: + name: string_validator + sha256: a278d038104aa2df15d0e09c47cb39a49f907260732067d0034dc2f2e4e2ac94 + url: "https://pub.dev" + source: hosted + version: "1.1.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + url: "https://pub.dev" + source: hosted + version: "3.3.0+3" + tapsell_plus: + dependency: "direct main" + description: + name: tapsell_plus + sha256: "328a60540b33d2b2f18d9578347cc154400518c3b31bfe6b58abc16c85353688" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + time: + dependency: transitive + description: + name: time + sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + toggle_switch: + dependency: "direct main" + description: + name: toggle_switch + sha256: dca04512d7c23ed320d6c5ede1211a404f177d54d353bf785b07d15546a86ce5 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + universal_html: + dependency: "direct main" + description: + name: universal_html + sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" + url: "https://pub.dev" + source: hosted + version: "2.2.4" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4" + url: "https://pub.dev" + source: hosted + version: "6.3.15" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + url_strategy: + dependency: "direct main" + description: + name: url_strategy + sha256: "6eff69fa0900b731a23552b38b54389f399d247dbb0998f2cbdf25bef6790a7c" + url: "https://pub.dev" + source: hosted + version: "0.3.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" + validator_regex: + dependency: "direct main" + description: + name: validator_regex + sha256: e4d292fba6f8c65b5b16b85dce6496518575668606491e707ac6a8103833e110 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" + url: "https://pub.dev" + source: hosted + version: "1.1.18" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + url: "https://pub.dev" + source: hosted + version: "1.1.16" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: "48941c8b05732f9582116b1c01850b74dbee1d8520cd7e34ad4609d6df666845" + url: "https://pub.dev" + source: hosted + version: "2.9.3" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: ae7d4f1b41e3ac6d24dd9b9d5d6831b52d74a61bdd90a7a6262a33d8bb97c29a + url: "https://pub.dev" + source: hosted + version: "2.8.2" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: "84b4752745eeccb6e75865c9aab39b3d28eb27ba5726d352d45db8297fbd75bc" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: df534476c341ab2c6a835078066fc681b8265048addd853a1e3c78740316a844 + url: "https://pub.dev" + source: hosted + version: "6.3.0" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "3ef40ea6d72434edbfdba4624b90fd3a80a0740d260667d91e7ecd2d79e13476" + url: "https://pub.dev" + source: hosted + version: "2.3.4" + visibility_detector: + dependency: transitive + description: + name: visibility_detector + sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 + url: "https://pub.dev" + source: hosted + version: "0.4.0+2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + url: "https://pub.dev" + source: hosted + version: "14.3.1" + wakelock_plus: + dependency: transitive + description: + name: wakelock_plus + sha256: "36c88af0b930121941345306d259ec4cc4ecca3b151c02e3a9e71aede83c615e" + url: "https://pub.dev" + source: hosted + version: "1.2.10" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: "70e780bc99796e1db82fe764b1e7dcb89a86f1e5b3afb1db354de50f2e41eb7a" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + webview_flutter: + dependency: transitive + description: + name: webview_flutter + sha256: ec81f57aa1611f8ebecf1d2259da4ef052281cb5ad624131c93546c79ccc7736 + url: "https://pub.dev" + source: hosted + version: "4.9.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558" + url: "https://pub.dev" + source: hosted + version: "3.16.9" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d + url: "https://pub.dev" + source: hosted + version: "2.10.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: bf0745adeaca48a3105473440cffade47720fe2d56514de4e86f0d363439c4a7 + url: "https://pub.dev" + source: hosted + version: "3.18.6" + win32: + dependency: transitive + description: + name: win32 + sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e + url: "https://pub.dev" + source: hosted + version: "5.10.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.8.0-0 <4.0.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..800cf89 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,209 @@ +name: hoshan +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: "none" # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.1+16 + +environment: + sdk: ">=3.3.1 <4.0.0" + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + + # Design + cupertino_icons: ^1.0.6 + flutter_svg: ^2.0.10+1 + carousel_slider: ^5.0.0 + smooth_page_indicator: ^1.2.0+3 + toggle_switch: ^2.3.0 + pinput: ^5.0.0 + flutter_spinkit: ^5.2.1 + cached_network_image: ^3.4.0 + animated_custom_dropdown: ^3.1.1 + shimmer: ^3.0.0 + markdown_widget: ^2.3.2+6 + image_cropper: ^9.0.0 + flutter_rating_bar: ^4.0.1 + flutter_launcher_icons: ^0.14.2 + fl_chart: ^0.69.2 + another_flushbar: ^1.12.30 + flutter_animate: ^4.5.2 + + # Core + dio: ^5.7.0 + pretty_dio_logger: ^1.4.0 + flutter_bloc: ^9.1.0 + equatable: ^2.0.5 + shared_preferences: ^2.2.3 + google_sign_in: ^6.2.1 + firebase_core: ^3.4.0 + firebase_messaging: ^15.1.0 + firebase_auth: ^5.2.0 + file_picker: ^9.2.0 + permission_handler: ^11.3.1 + cross_file: ^0.3.4+2 + image_picker: ^1.1.2 + path_provider: ^2.1.4 + intl: ^0.20.2 + shamsi_date: ^1.0.4 + easy_debounce: ^2.0.3 + url_launcher: ^6.3.1 + flutter_sound: ^9.17.8 + audioplayers: ^6.1.0 + flutter_downloader: ^1.11.8 + flutter_math_fork: ^0.7.4 + persian_number_utility: ^1.1.4 + app_links: ^6.3.3 + validator_regex: ^1.1.1 + string_validator: ^1.1.0 + excel: ^4.0.6 + rename_app: ^1.6.2 + share_plus: ^10.1.3 + go_router: ^14.6.3 + smart_auth: ^3.2.0 + percent_indicator: ^4.2.4 + flutter_media_downloader: ^2.0.0 + flutter_html: ^3.0.0 + flutter_highlight: ^0.7.0 + package_info_plus: ^8.1.3 + universal_html: ^2.2.4 + open_filex: ^4.6.0 + flutter_image_compress: ^2.4.0 + video_player: ^2.9.3 + chewie: ^1.10.0 + image_picker_android: ^0.8.12+21 + get_thumbnail_video: ^0.7.3 + url_strategy: ^0.3.0 + flutter_staggered_grid_view: ^0.7.0 + before_after: ^3.2.0 + popover: ^0.3.1 + background_downloader: ^9.2.0 + sentry_flutter: ^8.14.1 + # adivery: ^4.2.0 + tapsell_plus: ^2.3.2 + google_mobile_ads: ^4.0.0 + change_app_package_name: ^1.5.0 + flutter_inappwebview: ^6.0.0 + +flutter_launcher_icons: + android: false + adaptive_icon_background: "#ffffff" + ios: true + image_path: "assets/icon/launcher_icons/houshan-icon-primary.png" + min_sdk_android: 21 # android min sdk min:16, default 21 + web: + generate: true + image_path: "assets/icon/launcher_icons/houshan-icon-rounded.png" + background_color: "#FFFFFF" + theme_color: "#FFFFFF" + # windows: + # generate: true + # image_path: "path/to/image.png" + # icon_size: 48 # min:48, max:256, default: 48 + # macos: + # generate: true + # image_path: "path/to/image.png" + +dev_dependencies: + build_runner: + flutter_gen_runner: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + generate: true + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - assets/image/ + - assets/image/boardings/ + # - assets/image/products/ + - assets/image/splash/ + - assets/image/empty/ + - assets/icon/ + - assets/icon/launcher_icons/ + - assets/icon/gif/ + - assets/icon/outline/ + - assets/icon/signin/ + - assets/icon/bulk/ + - assets/icon/bold/ + - assets/icon/social/bold/ + - assets/icon/navbars/navigation/ + - assets/icon/navbars/navigation-dark/ + - assets/icon/navbars/navigation-light/ + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + fonts: + - family: Dana + fonts: + - asset: assets/font/IRANSansMobile-FaNum.ttf + - family: CustomIcons + fonts: + - asset: assets/font/CustomIcons.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages +flutter_gen: + parse_metadata: true + output: lib/core/gen/ + # directory_path_enabled: true + integrations: + flutter_svg: true diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..b88bb2e --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:hoshan/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/web/.well_known/assetlinks.json b/web/.well_known/assetlinks.json new file mode 100644 index 0000000..55fa145 --- /dev/null +++ b/web/.well_known/assetlinks.json @@ -0,0 +1,12 @@ +[ + { + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": "com.houshan.hoshan", + "sha256_cert_fingerprints": [ + "7B:34:6E:46:20:5D:61:91:8F:E5:F0:85:9F:41:4F:81:B8:91:CB:B9:6A:BA:49:DD:46:5D:BC:29:A1:7C:7B:06" + ] + } + } + ] \ No newline at end of file diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..5d56ec0 Binary files /dev/null and b/web/favicon.png differ diff --git a/web/flutter_bootstrap.js b/web/flutter_bootstrap.js new file mode 100644 index 0000000..32fb097 --- /dev/null +++ b/web/flutter_bootstrap.js @@ -0,0 +1,15 @@ +/ web/flutter_bootstrap.js + +{ { flutter_js } } +{ { flutter_build_config } } + + +_flutter.loader.load({ + onEntrypointLoaded: async function (engineInitializer) { + const appRunner = await engineInitializer.initializeEngine(); + + + + await appRunner.runApp(); + } +}); \ No newline at end of file diff --git a/web/home.html b/web/home.html new file mode 100644 index 0000000..57b9579 --- /dev/null +++ b/web/home.html @@ -0,0 +1,14 @@ + + + + + + Redirecting... + + + +

Redirecting to index.html...

+ + \ No newline at end of file diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..c2a639c Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..74a0641 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..c2a639c Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..74a0641 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/icons/houshan-icon-whie.png b/web/icons/houshan-icon-whie.png new file mode 100644 index 0000000..03a2370 Binary files /dev/null and b/web/icons/houshan-icon-whie.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..0f8bbaf --- /dev/null +++ b/web/index.html @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + Houshan + + + + + + + + +
+ { + } + loading +
+ + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..25eab93 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "Chat", + "short_name": "Chat", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} \ No newline at end of file