redesign podcast tab

This commit is contained in:
mohamadmahdi jebeli 2025-10-19 07:50:50 +03:30
parent 2da6dc8993
commit 386ed90101
22 changed files with 1914 additions and 719 deletions

View File

@ -0,0 +1,4 @@
<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.5067 23.17H13.4533C12.9067 23.17 12.4533 22.7166 12.4533 22.17C12.4533 21.6233 12.9067 21.17 13.4533 21.17H16.5067C17.08 21.17 17.5467 20.7033 17.5467 20.13C17.5467 19.5566 17.08 19.09 16.5067 19.09H13.4533C13.1333 19.09 12.8267 18.93 12.64 18.6766C12.4533 18.4233 12.4 18.0766 12.5067 17.77L13.52 14.7166C13.6533 14.3033 14.04 14.0366 14.4667 14.0366H18.5467C19.0933 14.0366 19.5467 14.49 19.5467 15.0366C19.5467 15.5833 19.0933 16.0366 18.5467 16.0366H15.1867L14.84 17.09H16.5067C18.1867 17.09 19.5467 18.45 19.5467 20.13C19.5467 21.81 18.1867 23.17 16.5067 23.17Z" fill="#666666"/>
<path d="M16 5.24978C15.8933 5.24978 15.7867 5.26311 15.68 5.26311L16.7733 3.90312C17.12 3.47645 17.0533 2.83645 16.6133 2.50311C16.1733 2.16978 15.56 2.22311 15.2133 2.66311L12.5867 5.94311C12.5733 5.95645 12.5733 5.96978 12.56 5.99645C12.52 6.04978 12.4933 6.11645 12.4667 6.16978C12.44 6.23645 12.4133 6.28978 12.4 6.34312C12.3867 6.40978 12.3867 6.46312 12.3867 6.52978C12.3867 6.59645 12.3867 6.66312 12.3867 6.72978C12.3867 6.75645 12.3867 6.76978 12.3867 6.79645C12.4 6.83645 12.4267 6.86311 12.44 6.90311C12.4667 6.96978 12.4933 7.02312 12.52 7.08978C12.56 7.14312 12.6 7.19645 12.6533 7.23645C12.68 7.27645 12.6933 7.31645 12.7333 7.34312C12.76 7.35645 12.7733 7.36978 12.8 7.38311C12.84 7.40978 12.8667 7.42311 12.9067 7.43645C12.9733 7.47645 13.0533 7.50311 13.1333 7.51645C13.1733 7.54311 13.2133 7.54311 13.2533 7.54311C13.2933 7.54311 13.32 7.55645 13.36 7.55645C13.4 7.55645 13.4267 7.54312 13.4533 7.52978C13.4933 7.52978 13.5333 7.54312 13.5733 7.52978C14.4267 7.32978 15.2267 7.23645 15.9867 7.23645C21.9733 7.23645 26.84 12.1031 26.84 18.0898C26.84 24.0764 21.9733 28.9431 15.9867 28.9431C10 28.9431 5.13333 24.0764 5.13333 18.0898C5.13333 15.7698 5.89333 13.5298 7.33333 11.6098C7.66667 11.1698 7.57333 10.5431 7.13333 10.2098C6.69333 9.87645 6.06667 9.96978 5.73333 10.4098C4.02667 12.6764 3.13333 15.3298 3.13333 18.0898C3.13333 25.1698 8.89333 30.9431 15.9867 30.9431C23.08 30.9431 28.84 25.1831 28.84 18.0898C28.84 10.9964 23.08 5.24978 16 5.24978Z" fill="#666666"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,4 @@
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.40771 16.6596C8.19021 16.6596 7.97271 16.6221 7.77021 16.5471C6.83271 16.2396 6.23271 15.2046 6.44271 14.2446L6.81021 11.8821C6.81771 11.8296 6.81771 11.7546 6.76521 11.6946C6.72771 11.6571 6.67521 11.6346 6.61521 11.6346H3.61521C2.88021 11.6346 2.24271 11.3271 1.86771 10.7946C1.50021 10.2771 1.42521 9.59463 1.66521 8.93463L3.45771 3.47463C3.73521 2.38713 4.89771 1.50963 6.09771 1.50963H8.94771C9.36771 1.50963 10.2827 1.63713 10.7702 2.12463L13.0427 3.87963L12.3527 4.77213L10.0277 2.97213C9.84021 2.78463 9.36771 2.63463 8.94771 2.63463H6.09771C5.42271 2.63463 4.69521 3.17463 4.54521 3.78213L2.73021 9.29463C2.61021 9.62463 2.63271 9.92463 2.79021 10.1421C2.95521 10.3746 3.25521 10.5096 3.62271 10.5096H6.62271C7.01271 10.5096 7.37271 10.6746 7.62021 10.9596C7.87521 11.2521 7.98771 11.6421 7.92771 12.0471L7.55271 14.4546C7.46271 14.8746 7.74771 15.3471 8.15271 15.4821C8.51271 15.6171 8.99271 15.4221 9.15771 15.1821L12.2327 10.6071L13.1627 11.2371L10.0877 15.8121C9.73521 16.3371 9.06771 16.6596 8.40771 16.6596Z" fill="#70706E"/>
<path d="M15.0227 13.6596H14.2727C12.8852 13.6596 12.2102 13.0071 12.2102 11.6721V4.32213C12.2102 2.98713 12.8852 2.33463 14.2727 2.33463H15.0227C16.4102 2.33463 17.0852 2.98713 17.0852 4.32213V11.6721C17.0852 13.0071 16.4102 13.6596 15.0227 13.6596ZM14.2727 3.45963C13.4552 3.45963 13.3352 3.65463 13.3352 4.32213V11.6721C13.3352 12.3396 13.4552 12.5346 14.2727 12.5346H15.0227C15.8402 12.5346 15.9602 12.3396 15.9602 11.6721V4.32213C15.9602 3.65463 15.8402 3.45963 15.0227 3.45963H14.2727Z" fill="#70706E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.4065 3.66701V10.4403C10.4065 10.707 10.3265 10.967 10.1799 11.187L8.35987 13.8937C8.0732 14.327 7.35987 14.6337 6.7532 14.407C6.09987 14.187 5.66653 13.4537 5.80653 12.8003L6.1532 10.6203C6.17987 10.4203 6.12654 10.2403 6.0132 10.1003C5.89987 9.97367 5.7332 9.89367 5.5532 9.89367H2.8132C2.28653 9.89367 1.8332 9.68034 1.56653 9.307C1.3132 8.94701 1.26653 8.48034 1.4332 8.007L3.0732 3.01367C3.27987 2.18701 4.17987 1.51367 5.0732 1.51367H7.6732C8.11987 1.51367 8.74653 1.66701 9.0332 1.95367L9.88653 2.61367C10.2132 2.86701 10.4065 3.25367 10.4065 3.66701Z" fill="#C41212"/>
<path d="M12.5266 11.7399H13.2132C14.2466 11.7399 14.6666 11.3399 14.6666 10.3533V3.65327C14.6666 2.6666 14.2466 2.2666 13.2132 2.2666H12.5266C11.4932 2.2666 11.0732 2.6666 11.0732 3.65327V10.3599C11.0732 11.3399 11.4932 11.7399 12.5266 11.7399Z" fill="#C41212"/>
</svg>

After

Width:  |  Height:  |  Size: 956 B

View File

@ -0,0 +1,5 @@
<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26.2533 10.4096C25.92 9.96964 25.2933 9.87631 24.8533 10.2096C24.4133 10.543 24.32 11.1696 24.6533 11.6096C26.0933 13.5296 26.8533 15.7696 26.8533 18.0896C26.8533 24.0763 21.9867 28.943 16 28.943C10.0133 28.943 5.14666 24.0763 5.14666 18.0896C5.14666 12.103 10.0133 7.24964 16 7.24964C16.7733 7.24964 17.56 7.34298 18.4133 7.54297C18.4533 7.55631 18.4933 7.54297 18.5467 7.54297C18.5733 7.54297 18.6133 7.56964 18.64 7.56964C18.68 7.56964 18.7067 7.55631 18.7467 7.55631C18.8 7.55631 18.84 7.54298 18.88 7.52964C18.9467 7.51631 19.0133 7.48964 19.08 7.44964C19.12 7.42298 19.1733 7.40964 19.2133 7.38298C19.2267 7.36964 19.2533 7.36964 19.2667 7.35631C19.3067 7.32964 19.32 7.28964 19.3467 7.26297C19.4 7.20964 19.44 7.16964 19.48 7.10297C19.52 7.04964 19.5333 6.98298 19.56 6.92964C19.5733 6.88964 19.6 6.84964 19.6133 6.80964C19.6133 6.78298 19.6133 6.76964 19.6133 6.74298C19.6267 6.67631 19.6267 6.60964 19.6133 6.54297C19.6133 6.47631 19.6133 6.42298 19.6 6.35631C19.5867 6.30298 19.56 6.24964 19.5333 6.18297C19.5067 6.11631 19.48 6.04964 19.44 5.99631C19.4267 5.96964 19.4267 5.95631 19.4133 5.94298L16.7733 2.64964C16.4267 2.22297 15.8 2.15631 15.3733 2.48964C14.9467 2.83631 14.88 3.46298 15.2133 3.88964L16.3067 5.24964C16.2 5.24964 16.0933 5.23631 15.9867 5.23631C8.90666 5.23631 3.13333 10.9963 3.13333 18.0896C3.13333 25.183 8.89333 30.943 15.9867 30.943C23.08 30.943 28.84 25.183 28.84 18.0896C28.8533 15.3296 27.9467 12.6763 26.2533 10.4096Z" fill="#666666"/>
<path d="M12.72 22.8366C12.1733 22.8366 11.72 22.3833 11.72 21.8366V17.3166L11.4667 17.61C11.0933 18.0233 10.4667 18.05 10.0533 17.69C9.64 17.3166 9.61333 16.69 9.97333 16.2766L11.9733 14.05C12.2533 13.7433 12.6933 13.6366 13.08 13.7833C13.4667 13.93 13.72 14.3033 13.72 14.7166V21.85C13.72 22.3966 13.28 22.8366 12.72 22.8366Z" fill="#666666"/>
<path d="M18.6667 22.8365C16.64 22.8365 15 21.1965 15 19.1698V17.3698C15 15.3431 16.64 13.7031 18.6667 13.7031C20.6933 13.7031 22.3333 15.3431 22.3333 17.3698V19.1698C22.3333 21.1965 20.6933 22.8365 18.6667 22.8365ZM18.6667 15.7165C17.7467 15.7165 17 16.4631 17 17.3831V19.1831C17 20.1031 17.7467 20.8498 18.6667 20.8498C19.5867 20.8498 20.3333 20.1031 20.3333 19.1831V17.3831C20.3333 16.4631 19.5867 15.7165 18.6667 15.7165Z" fill="#666666"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,4 @@
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5177 16.6596H9.6677C9.2477 16.6596 8.3327 16.5321 7.8452 16.0446L5.5727 14.2896L6.2627 13.3971L8.5877 15.1971C8.7752 15.3771 9.2477 15.5271 9.6677 15.5271H12.5177C13.1927 15.5271 13.9202 14.9871 14.0702 14.3796L15.8852 8.86712C16.0052 8.53712 15.9827 8.23712 15.8252 8.01962C15.6602 7.78712 15.3602 7.65212 14.9927 7.65212H11.9927C11.6027 7.65212 11.2427 7.48712 10.9952 7.20212C10.7402 6.90962 10.6277 6.51962 10.6877 6.11462L11.0627 3.70712C11.1527 3.28712 10.8677 2.81462 10.4627 2.67962C10.0952 2.54462 9.6227 2.73962 9.4577 2.97962L6.3827 7.55462L5.4527 6.93212L8.5277 2.35712C9.0002 1.65212 10.0352 1.31462 10.8452 1.62212C11.7827 1.92962 12.3827 2.96462 12.1727 3.92462L11.8052 6.28712C11.7977 6.33962 11.7977 6.41462 11.8502 6.47462C11.8877 6.51212 11.9402 6.53462 12.0002 6.53462H15.0002C15.7352 6.53462 16.3727 6.84212 16.7477 7.37462C17.1152 7.89212 17.1902 8.57462 16.9502 9.23462L15.1577 14.6946C14.8802 15.7821 13.7252 16.6596 12.5177 16.6596Z" fill="#70706E"/>
<path d="M4.34265 15.8346H3.59265C2.20515 15.8346 1.53015 15.1821 1.53015 13.8471V6.49713C1.53015 5.16213 2.20515 4.50963 3.59265 4.50963H4.34265C5.73015 4.50963 6.40515 5.16213 6.40515 6.49713V13.8471C6.40515 15.1821 5.73015 15.8346 4.34265 15.8346ZM3.59265 5.63463C2.77515 5.63463 2.65515 5.82963 2.65515 6.49713V13.8471C2.65515 14.5146 2.77515 14.7096 3.59265 14.7096H4.34265C5.16015 14.7096 5.28015 14.5146 5.28015 13.8471V6.49713C5.28015 5.82963 5.16015 5.63463 4.34265 5.63463H3.59265Z" fill="#70706E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.59326 12.3363V5.56292C5.59326 5.29625 5.67326 5.03625 5.81993 4.81625L7.63993 2.10959C7.92659 1.67625 8.63993 1.36959 9.24659 1.59625C9.89993 1.81625 10.3333 2.54959 10.1933 3.20292L9.84659 5.38292C9.81993 5.58292 9.87326 5.76292 9.98659 5.90292C10.0999 6.02959 10.2666 6.10959 10.4466 6.10959H13.1866C13.7133 6.10959 14.1666 6.32292 14.4333 6.69625C14.6866 7.05625 14.7333 7.52292 14.5666 7.99625L12.9266 12.9896C12.7199 13.8163 11.8199 14.4896 10.9266 14.4896H8.32659C7.87993 14.4896 7.25326 14.3363 6.96659 14.0496L6.11326 13.3896C5.78659 13.1429 5.59326 12.7496 5.59326 12.3363Z" fill="#03B4A3"/>
<path d="M3.47325 4.2627H2.78659C1.75325 4.2627 1.33325 4.6627 1.33325 5.64936V12.356C1.33325 13.3427 1.75325 13.7427 2.78659 13.7427H3.47325C4.50659 13.7427 4.92659 13.3427 4.92659 12.356V5.64936C4.92659 4.6627 4.50659 4.2627 3.47325 4.2627Z" fill="#03B4A3"/>
</svg>

After

Width:  |  Height:  |  Size: 977 B

View File

@ -0,0 +1,3 @@
<svg width="56" height="57" viewBox="0 0 56 57" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M27.93 5.27637C15.05 5.27637 4.59666 15.7297 4.59666 28.6097C4.59666 41.4897 15.05 51.943 27.93 51.943C40.81 51.943 51.2633 41.4897 51.2633 28.6097C51.2633 15.7297 40.8333 5.27637 27.93 5.27637ZM25.0133 35.6797C25.0133 36.7997 24.5467 37.243 23.3567 37.243H20.3233C19.1333 37.243 18.6667 36.7997 18.6667 35.6797V21.5397C18.6667 20.4197 19.1333 19.9764 20.3233 19.9764H23.3333C24.5233 19.9764 24.99 20.4197 24.99 21.5397V35.6797H25.0133ZM37.3333 35.6797C37.3333 36.7997 36.8667 37.243 35.6767 37.243H32.6667C31.4767 37.243 31.01 36.7997 31.01 35.6797V21.5397C31.01 20.4197 31.4767 19.9764 32.6667 19.9764H35.6767C36.8667 19.9764 37.3333 20.4197 37.3333 21.5397V35.6797Z" fill="#007EA7"/>
</svg>

After

Width:  |  Height:  |  Size: 799 B

View File

@ -0,0 +1,3 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24 0.666992C11.12 0.666992 0.666672 11.1203 0.666672 24.0003C0.666672 36.8803 11.12 47.3337 24 47.3337C36.88 47.3337 47.3333 36.8803 47.3333 24.0003C47.3333 11.1203 36.88 0.666992 24 0.666992ZM30.2067 28.037L27.22 29.7637L24.2333 31.4903C20.3833 33.707 17.2333 31.887 17.2333 27.4537V24.0003V20.547C17.2333 16.0903 20.3833 14.2937 24.2333 16.5103L27.22 18.237L30.2067 19.9637C34.0567 22.1803 34.0567 25.8203 30.2067 28.037Z" fill="#007EA7"/>
</svg>

After

Width:  |  Height:  |  Size: 555 B

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.50978 4.22989L18.0698 8.50989C21.9098 10.4299 21.9098 13.5699 18.0698 15.4899L9.50978 19.7699C3.74978 22.6499 1.39978 20.2899 4.27978 14.5399L5.14978 12.8099C5.36978 12.3699 5.36978 11.6399 5.14978 11.1999L4.27978 9.45989C1.39978 3.70989 3.75978 1.34989 9.50978 4.22989Z" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.43994 12H10.8399" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 587 B

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.4901 4.23941L5.93006 8.51941C2.09006 10.4394 2.09006 13.5794 5.93006 15.4994L14.4901 19.7794C20.2501 22.6594 22.6001 20.2994 19.7201 14.5494L18.8501 12.8194C18.6301 12.3794 18.6301 11.6494 18.8501 11.2094L19.7201 9.46941C22.6001 3.71941 20.2401 1.35941 14.4901 4.23941Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.5601 12.0098H13.1601" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 588 B

View File

@ -196,7 +196,7 @@ extension DidvanColorScheme on ColorScheme {
: const Color(0xFFE0E0E0);
Color get cardBorder => brightness == Brightness.dark
? const Color(0xFF5C5C5C)
: const Color(0xFFEBEBEB);
: const Color.fromARGB(255, 184, 184, 184);
Color get disabledBackground => brightness == Brightness.dark
? const Color(0xFF333333)
: const Color(0xFFE0E0E0);

View File

@ -27,6 +27,7 @@ class MediaService {
static StudioDetailsData? currentPodcast;
static StudioRequestArgs? podcastPlaylistArgs;
static int maxSizeInBytes = 15 * 1024 * 1024;
static bool isPlayingFromFeaturedCard = false;
static Duration? get duration => audioPlayer.duration;
@ -99,6 +100,7 @@ class MediaService {
audioPlayerTag = null;
currentPodcast = null;
podcastPlaylistArgs = null;
isPlayingFromFeaturedCard = false;
await MediaService.audioPlayer.stop();
}

View File

@ -1,18 +1,16 @@
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/constants/assets.dart';
import 'package:didvan/models/view/app_bar_data.dart';
import 'package:didvan/views/comments/comments_state.dart';
import 'package:didvan/views/comments/widgets/comment.dart';
import 'package:didvan/views/widgets/animated_visibility.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:didvan/views/widgets/didvan/scaffold.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/shimmer_placeholder.dart';
import 'package:didvan/views/widgets/state_handlers/empty_state.dart';
import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
class Comments extends StatefulWidget {
@ -32,15 +30,17 @@ class _CommentsState extends State<Comments> {
@override
void initState() {
final state = context.read<CommentsState>();
state.itemId = widget.pageData['id'];
state.type = widget.pageData['type'];
state.onCommentsChanged = widget.pageData['onCommentsChanged'];
Future.delayed(
Duration.zero,
() => state.getComments(),
);
super.initState();
if (_isPage) {
final state = context.read<CommentsState>();
state.itemId = widget.pageData['id'];
state.type = widget.pageData['type'];
state.onCommentsChanged = widget.pageData['onCommentsChanged'];
Future.delayed(
Duration.zero,
() => state.getComments(),
);
}
}
bool get _isPage => widget.pageData['isPage'] != false;
@ -56,52 +56,42 @@ class _CommentsState extends State<Comments> {
}
_bottomPadding = bottomViewInset;
return Material(
child: Stack(
children: [
DidvanScaffold(
hidePlayer: true,
physics: const BouncingScrollPhysics(),
backgroundColor: Theme.of(context).colorScheme.surface,
appBarData: _isPage
? AppBarData(
hasBack: true,
title: 'نظرات',
subtitle: widget.pageData['title'],
)
: null,
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 92),
slivers: [
Consumer<CommentsState>(
builder: (context, state, child) =>
SliverStateHandler<CommentsState>(
onRetry: state.getComments,
state: state,
itemPadding: const EdgeInsets.symmetric(vertical: 16),
childCount: state.comments.length,
placeholder: const _CommentPlaceholder(),
centerEmptyState: _isPage,
enableEmptyState: state.comments.isEmpty,
emptyState: EmptyState(
asset: Assets.emptyChat,
title: 'اولین نظر را بنویسید...',
),
builder: (context, state, index) => Comment(
key: ValueKey(
state.comments[index].id.toString() +
state.comments[index].text,
),
focusNode: _focusNode,
comment: state.comments[index],
),
),
child: DidvanScaffold(
hidePlayer: true,
physics: const BouncingScrollPhysics(),
backgroundColor: Theme.of(context).colorScheme.surface,
appBarData: _isPage
? AppBarData(
hasBack: true,
title: 'نظرات',
subtitle: widget.pageData['title'],
)
: null,
padding: _isPage ? const EdgeInsets.only(bottom: 92) : EdgeInsets.zero,
slivers: [
Consumer<CommentsState>(
builder: (context, state, child) =>
SliverStateHandler<CommentsState>(
onRetry: state.getComments,
state: state,
itemPadding: const EdgeInsets.only(top: 16, bottom: 16),
childCount: state.comments.length,
placeholder: const _CommentPlaceholder(),
centerEmptyState: _isPage,
enableEmptyState: state.comments.isEmpty,
emptyState: EmptyState(
asset: Assets.emptyChat,
title: 'اولین نظر را بنویسید...',
),
],
),
Positioned(
left: 0,
right: 0,
bottom: MediaQuery.of(context).viewInsets.bottom,
child: _MessageBox(focusNode: _focusNode),
builder: (context, state, index) => Comment(
key: ValueKey(
state.comments[index].id.toString() +
state.comments[index].text,
),
focusNode: _focusNode,
comment: state.comments[index],
),
),
),
],
),
@ -109,77 +99,86 @@ class _CommentsState extends State<Comments> {
}
}
class _MessageBox extends StatefulWidget {
class CommentMessageBox extends StatefulWidget {
final FocusNode focusNode;
const _MessageBox({Key? key, required this.focusNode}) : super(key: key);
const CommentMessageBox({Key? key, required this.focusNode})
: super(key: key);
@override
State<_MessageBox> createState() => _MessageBoxState();
State<CommentMessageBox> createState() => _CommentMessageBoxState();
}
class _MessageBoxState extends State<_MessageBox> {
class _CommentMessageBoxState extends State<CommentMessageBox> {
final _controller = TextEditingController();
@override
Widget build(BuildContext context) {
final state = context.watch<CommentsState>();
return Column(
children: [
AnimatedVisibility(
duration: DesignConfig.lowAnimationDuration,
isVisible: state.showReplyBox,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border(
top: BorderSide(
color: Theme.of(context).colorScheme.border,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (state.replyingTo != null)
DidvanText(
'پاسخ به ${state.replyingTo!.fullName}:',
color: Theme.of(context).colorScheme.caption,
style: Theme.of(context).textTheme.bodySmall,
AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
child: state.showReplyBox
? Container(
key: const ValueKey('reply_box'),
margin:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).colorScheme.primary,
width: 1,
),
),
const Spacer(),
DidvanIconButton(
gestureSize: 24,
color: Theme.of(context).colorScheme.caption,
icon: DidvanIcons.close_regular,
onPressed: () {
state.commentId = null;
state.replyingTo = null;
state.showReplyBox = false;
state.update();
},
),
],
),
),
child: Row(
children: [
const Icon(
Icons.reply,
size: 16,
color: Color.fromARGB(255, 194, 194, 194),
),
const SizedBox(width: 8),
Expanded(
child: DidvanText(
'پاسخ به ${state.replyingTo?.fullName ?? ""}',
style: const TextStyle(
fontSize: 13,
color: Color.fromARGB(255, 194, 194, 194),
fontWeight: FontWeight.w600,
),
),
),
GestureDetector(
onTap: () {
state.commentId = null;
state.replyingTo = null;
state.showReplyBox = false;
state.update();
},
child: const Icon(
Icons.close,
size: 16,
color: Colors.grey,
),
),
],
),
)
: const SizedBox.shrink(key: ValueKey('empty')),
),
Container(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border(
top: BorderSide(
color: Theme.of(context).colorScheme.border,
),
),
color: const Color.fromARGB(255, 243, 243, 243),
border: Border.all(color: Theme.of(context).colorScheme.border),
borderRadius: BorderRadius.circular(32),
),
child: Row(
children: [
DidvanIconButton(
onPressed: () => _onSend(state),
icon: DidvanIcons.send_solid,
size: 24,
color: Theme.of(context).colorScheme.focusedBorder,
),
Expanded(
child: TextField(
focusNode: widget.focusNode,
@ -191,13 +190,52 @@ class _MessageBoxState extends State<_MessageBox> {
onSubmitted: (value) => _onSend(state),
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'پیام خود را ارسال کنید',
hintStyle: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context).colorScheme.disabledText),
hintText: 'دیدگاه‌های خود را بنویسید...',
hintStyle: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: Colors.black),
),
onChanged: (value) => state.text = value,
),
),
ElevatedButton(
onPressed: () => _onSend(state),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(32),
),
padding: EdgeInsets.zero,
minimumSize: const Size(10, 20),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
backgroundColor: Theme.of(context).primaryColor,
elevation: 0,
),
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 10, 20, 10),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'ارسال',
style:
Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Colors.white,
fontWeight: FontWeight.normal,
),
),
const SizedBox(width: 8),
SvgPicture.asset(
'lib/assets/icons/send2.svg',
color: Colors.white,
),
],
),
),
),
),
],
),
),
@ -206,9 +244,10 @@ class _MessageBoxState extends State<_MessageBox> {
}
void _onSend(CommentsState state) {
if (state.text.replaceAll(' ', '').isNotEmpty) {
if (state.text.trim().isNotEmpty) {
state.addComment();
_controller.text = '';
_controller.clear();
FocusScope.of(context).unfocus();
}
}
}

View File

@ -15,6 +15,7 @@ import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/ink_wrapper.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
class Comment extends StatefulWidget {
@ -70,142 +71,168 @@ class CommentState extends State<Comment> {
: BorderSide.none,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isReply) const SizedBox(width: 12),
if (comment.user.photo == null)
const Icon(DidvanIcons.avatar_light),
if (comment.user.photo != null)
SkeletonImage(
imageUrl: comment.user.photo,
height: 24,
width: 24,
borderRadius: DesignConfig.highBorderRadius,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (isReply) const SizedBox(width: 12),
if (comment.user.photo == null)
const Icon(
DidvanIcons.avatar_light,
size: 50,
),
if (comment.user.photo != null)
SkeletonImage(
imageUrl: comment.user.photo,
height: 50,
width: 50,
borderRadius: DesignConfig.highBorderRadius,
),
const SizedBox(width: 6),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(
comment.user.fullName,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
color: Color.fromARGB(255, 102, 102, 102)),
),
const SizedBox(height: 4),
DidvanText(
DateTimeUtils.momentGenerator(comment.createdAt),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
color: const Color.fromARGB(255, 0, 126, 167),
),
],
),
const Spacer(),
DidvanIconButton(
size: 18,
gestureSize: 24,
icon: DidvanIcons.menu_light,
onPressed: () => _showCommentActions(comment),
),
],
),
const SizedBox(height: 8),
if (isReply)
DidvanText(
comment.user.fullName,
style: Theme.of(context).textTheme.bodyLarge,
),
const Spacer(),
DidvanText(
DateTimeUtils.momentGenerator(comment.createdAt),
'پاسخ به ${comment.toUser.fullName}',
style: Theme.of(context).textTheme.bodySmall,
color: Theme.of(context).colorScheme.caption,
),
const SizedBox(width: 4),
DidvanIconButton(
size: 18,
gestureSize: 24,
icon: DidvanIcons.menu_light,
onPressed: () => _showCommentActions(comment),
),
],
),
const SizedBox(height: 8),
if (isReply)
const SizedBox(height: 8),
DidvanText(
'پاسخ به ${comment.toUser.fullName}',
style: Theme.of(context).textTheme.bodySmall,
color: Theme.of(context).colorScheme.caption,
comment.text,
color: const Color.fromARGB(255, 102, 102, 102),
),
const SizedBox(height: 8),
DidvanText(comment.text),
const SizedBox(height: 8),
if (comment.status == 2)
Row(
children: [
Icon(
Icons.circle,
color: Theme.of(context)
.colorScheme
.secondary
.withValues(alpha: 0.3),
size: 18,
),
const SizedBox(width: 4),
DidvanText(
'در انتظار تایید',
color: Theme.of(context)
.colorScheme
.secondary
.withValues(alpha: 0.3),
),
],
),
const SizedBox(height: 8),
if (_comment.status != 2)
Row(
children: [
InkWrapper(
onPressed: () {
state.commentId = _comment.id;
state.replyingTo = comment.user;
state.showReplyBox = true;
state.update();
widget.focusNode.requestFocus();
},
child: DidvanText(
'پاسخ',
style: Theme.of(context).textTheme.bodyLarge,
color: Theme.of(context).colorScheme.primary,
const SizedBox(height: 8),
if (comment.status == 2)
Row(
children: [
Icon(
Icons.circle,
color: Theme.of(context)
.colorScheme
.secondary
.withValues(alpha: 0.3),
size: 18,
),
),
if (!isReply) const SizedBox(width: 20),
if (!isReply && comment.replies.isNotEmpty)
InkWrapper(
onPressed: () => setState(
() => _showSubComments = !_showSubComments,
const SizedBox(width: 4),
DidvanText(
'در انتظار تایید',
color: Theme.of(context)
.colorScheme
.secondary
.withValues(alpha: 0.3),
),
],
),
const SizedBox(height: 8),
if (_comment.status != 2)
Row(
children: [
_FeedbackButtons(
likeCount: comment.feedback.like,
dislikeCount: comment.feedback.dislike,
likeValue: comment.liked,
dislikeValue: comment.disliked,
onFeedback:
(like, dislike, likeCount, dislikeCount) =>
state.feedback(
id: _comment.id,
like: like,
dislike: dislike,
likeCount: likeCount,
dislikeCount: dislikeCount,
replyId: isReply ? comment.id : null,
),
child: Row(
children: [
DidvanText(
'پاسخ‌ها(${comment.replies.length})',
style: Theme.of(context).textTheme.bodyLarge,
color: Theme.of(context).colorScheme.primary,
),
AnimatedRotation(
duration: DesignConfig.lowAnimationDuration,
turns: _showSubComments ? 0.5 : 0,
child: Icon(
DidvanIcons.angle_down_regular,
),
const SizedBox(
width: 20,
),
InkWrapper(
onPressed: () {
state.commentId = _comment.id;
state.replyingTo = comment.user;
state.showReplyBox = true;
state.update();
widget.focusNode.requestFocus();
},
child: DidvanText(
'پاسخ',
style: Theme.of(context).textTheme.bodySmall,
color: const Color.fromARGB(255, 102, 102, 102),
),
),
if (!isReply) const SizedBox(width: 20),
if (!isReply && comment.replies.isNotEmpty)
InkWrapper(
onPressed: () => setState(
() => _showSubComments = !_showSubComments,
),
child: Row(
children: [
DidvanText(
'پاسخ‌ها(${comment.replies.length})',
style:
Theme.of(context).textTheme.bodyLarge,
color:
Theme.of(context).colorScheme.primary,
),
),
],
AnimatedRotation(
duration: DesignConfig.lowAnimationDuration,
turns: _showSubComments ? 0.5 : 0,
child: Icon(
DidvanIcons.angle_down_regular,
color:
Theme.of(context).colorScheme.primary,
),
),
],
),
),
),
const Spacer(),
_FeedbackButtons(
likeCount: comment.feedback.like,
dislikeCount: comment.feedback.dislike,
likeValue: comment.liked,
dislikeValue: comment.disliked,
onFeedback:
(like, dislike, likeCount, dislikeCount) =>
state.feedback(
id: _comment.id,
like: like,
dislike: dislike,
likeCount: likeCount,
dislikeCount: dislikeCount,
replyId: isReply ? comment.id : null,
),
),
],
),
],
],
),
],
),
),
),
],
],
),
),
);
@ -285,18 +312,10 @@ class _FeedbackButtonsState extends State<_FeedbackButtons> {
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
DidvanText(
_likeCount.toString(),
style: Theme.of(context).textTheme.bodySmall,
color: Theme.of(context).colorScheme.caption,
),
const SizedBox(width: 4),
DidvanIconButton(
icon: _likeValue ? DidvanIcons.like_solid : DidvanIcons.like_regular,
color: _likeValue ? Theme.of(context).colorScheme.primary : null,
gestureSize: 24,
onPressed: () {
GestureDetector(
onTap: () {
setState(() {
if (_likeValue) {
_likeCount--;
@ -312,21 +331,76 @@ class _FeedbackButtonsState extends State<_FeedbackButtons> {
widget.onFeedback(
_likeValue, _dislikeValue, _likeCount, _dislikeCount);
},
),
const SizedBox(width: 16),
DidvanText(
_dislikeCount.toString(),
style: Theme.of(context).textTheme.bodySmall,
color: Theme.of(context).colorScheme.caption,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
transform: _likeValue
? (Matrix4.identity()..scale(1.2))
: Matrix4.identity(),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation,
child: RotationTransition(
turns:
Tween<double>(begin: 0.8, end: 1.0).animate(animation),
child: child,
),
);
},
child: SvgPicture.asset(
_likeValue
? 'lib/assets/icons/likefill.svg'
: 'lib/assets/icons/like.svg',
key: ValueKey(_likeValue),
color:
_likeValue ? const Color.fromARGB(255, 43, 178, 74) : null,
width: 16,
height: 16,
),
),
),
),
const SizedBox(width: 4),
DidvanIconButton(
icon: _dislikeValue
? DidvanIcons.dislike_solid
: DidvanIcons.dislike_regular,
color: _dislikeValue ? Theme.of(context).colorScheme.secondary : null,
gestureSize: 24,
onPressed: () {
AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 200),
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color:
_likeValue ? const Color.fromARGB(255, 43, 178, 74) : null,
fontWeight: _likeValue ? FontWeight.bold : FontWeight.normal,
),
child: DidvanText(
'پسندیدم',
color: _likeValue ? const Color.fromARGB(255, 43, 178, 74) : null,
style: const TextStyle(fontSize: 11),
),
),
const SizedBox(
width: 5,
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
transitionBuilder: (Widget child, Animation<double> animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.5),
end: const Offset(0, 0),
).animate(animation),
child: FadeTransition(opacity: animation, child: child),
);
},
child: DidvanText('(${_likeCount.toString()})',
key: ValueKey(_likeCount),
style: Theme.of(context).textTheme.bodySmall!.copyWith(
fontSize: 11,
color: _likeValue
? const Color.fromARGB(255, 43, 178, 74)
: null,
)),
),
const SizedBox(width: 12),
GestureDetector(
onTap: () {
setState(() {
if (_dislikeValue) {
_dislikeCount--;
@ -342,7 +416,78 @@ class _FeedbackButtonsState extends State<_FeedbackButtons> {
widget.onFeedback(
_likeValue, _dislikeValue, _likeCount, _dislikeCount);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
transform: _dislikeValue
? (Matrix4.identity()..scale(1.2))
: Matrix4.identity(),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation,
child: RotationTransition(
turns:
Tween<double>(begin: 0.8, end: 1.0).animate(animation),
child: child,
),
);
},
child: SvgPicture.asset(
_dislikeValue
? 'lib/assets/icons/dislike_fill.svg'
: 'lib/assets/icons/dislike.svg',
key: ValueKey(_dislikeValue),
color: _dislikeValue
? const Color.fromARGB(255, 196, 18, 18)
: null,
width: 16,
height: 16,
),
),
),
),
const SizedBox(width: 4),
AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 200),
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: _dislikeValue
? const Color.fromARGB(255, 196, 18, 18)
: null,
fontWeight: _dislikeValue ? FontWeight.bold : FontWeight.normal,
),
child: DidvanText(
'نپسندیدم',
color:
_dislikeValue ? const Color.fromARGB(255, 196, 18, 18) : null,
fontSize: 11,
),
),
const SizedBox(
width: 5,
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
transitionBuilder: (Widget child, Animation<double> animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.5),
end: const Offset(0, 0),
).animate(animation),
child: FadeTransition(opacity: animation, child: child),
);
},
child: DidvanText(
'(${_dislikeCount.toString()})',
key: ValueKey(_dislikeCount),
style: Theme.of(context).textTheme.bodySmall!.copyWith(
fontSize: 11,
),
color:
_dislikeValue ? const Color.fromARGB(255, 196, 18, 18) : null,
),
),
const SizedBox(width: 4),
],
);
}

View File

@ -1,9 +1,16 @@
import 'dart:async';
import 'package:didvan/models/requests/studio.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/views/home/media/widgets/featured_podcast_card.dart';
import 'package:didvan/views/home/media/widgets/podcast_list_card.dart';
import 'package:didvan/views/podcasts/podcasts_state.dart';
import 'package:didvan/views/widgets/overview/podcast.dart';
import 'package:didvan/views/widgets/item_title.dart';
// import 'package:didvan/views/widgets/overview/podcast.dart';
import 'package:didvan/views/widgets/state_handlers/empty_result.dart';
import 'package:didvan/views/widgets/state_handlers/state_handler.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart';
class PodcastTabPage extends StatefulWidget {
@ -14,6 +21,10 @@ class PodcastTabPage extends StatefulWidget {
}
class _PodcastTabPageState extends State<PodcastTabPage> {
int _currentFeaturedIndex = 0;
Timer? _rotationTimer;
bool _isPlaying = false;
@override
void initState() {
super.initState();
@ -23,6 +34,111 @@ class _PodcastTabPageState extends State<PodcastTabPage> {
});
}
void _startRotation() {
_rotationTimer?.cancel();
_rotationTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
final state = context.read<PodcastsState>();
if (state.studios.isNotEmpty && mounted && !_isPlaying) {
setState(() {
_currentFeaturedIndex =
(_currentFeaturedIndex + 1) % state.studios.length;
});
}
});
}
void _onPlayStateChanged(bool isPlaying) {
setState(() {
_isPlaying = isPlaying;
});
}
void _showSortDialog() {
final state = context.read<PodcastsState>();
showModalBottomSheet(
backgroundColor: Colors.white,
context: context,
builder: (context) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Text('تازه‌ترین‌ها'),
onTap: () {
state.selectedSortTypeIndex = 0;
state.getStudios(page: 1);
Navigator.pop(context);
},
),
ListTile(
title: const Text('قدیمی‌ترین‌ها'),
onTap: () {
state.selectedSortTypeIndex = 1;
state.getStudios(page: 1);
Navigator.pop(context);
},
),
ListTile(
title: const Text('پربازدیدترین‌ها'),
onTap: () {
state.selectedSortTypeIndex = 2;
state.getStudios(page: 1);
Navigator.pop(context);
},
),
ListTile(
title: const Text('پربحث‌ترین‌ها'),
onTap: () {
state.selectedSortTypeIndex = 3;
state.getStudios(page: 1);
Navigator.pop(context);
},
),
],
),
);
},
);
}
void _navigateToDetails(int index) {
final state = context.read<PodcastsState>();
final item = state.studios[index];
Navigator.pushNamed(
context,
Routes.studioDetails,
arguments: {
'id': item.id,
'type': item.type,
'onMarkChanged': (int id, bool value, [bool shouldUpdate = true]) {
context.read<PodcastsState>().changeMark(id, value, shouldUpdate);
},
'args': StudioRequestArgs(
page: state.page,
type: state.type,
search: state.search,
order: state.order,
asc: state.selectedSortTypeIndex == 1,
endDate: state.endDate?.split(' ').first,
startDate: state.startDate?.split(' ').first,
),
},
);
}
@override
void dispose() {
_rotationTimer?.cancel();
if (MediaService.isPlayingFromFeaturedCard && MediaService.audioPlayer.playing) {
MediaService.audioPlayer.pause();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final state = context.watch<PodcastsState>();
@ -33,31 +149,81 @@ class _PodcastTabPageState extends State<PodcastTabPage> {
onNewSearch: () {},
),
enableEmptyState: state.studios.isEmpty,
placeholder: PodcastOverview.placeholder,
builder: (context, state) {
return ListView.builder(
itemCount: state.studios.length,
itemBuilder: (context, index) {
final podcast = state.studios[index];
return Padding(
padding: const EdgeInsets.only(
bottom: 8,
left: 16,
right: 16,
),
child: PodcastOverview(
podcast: podcast,
onMarkChanged: state.changeMark,
studioRequestArgs: StudioRequestArgs(
page: state.page,
order: state.order,
search: state.search,
type: state.type,
asc: state.selectedSortTypeIndex == 1,
if (state.studios.isEmpty) {
return const SizedBox.shrink();
}
if (_rotationTimer == null || !_rotationTimer!.isActive) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_startRotation();
});
}
return SingleChildScrollView(
child: Column(
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(opacity: animation, child: child);
},
child: FeaturedPodcastCard(
key: ValueKey<int?>(state.studios[_currentFeaturedIndex].id),
podcast: state.studios[_currentFeaturedIndex],
onTap: () => _navigateToDetails(_currentFeaturedIndex),
onPlayStateChanged: _onPlayStateChanged,
),
),
);
},
if (state.studios.length > 1)
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const ItemTitle(
title: 'همه پادکست‌ها',
color: Color.fromARGB(255, 0, 53, 70),
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 18),
),
Row(
children: [
GestureDetector(
onTap: _showSortDialog,
child: Text(
state.orderString,
style: const TextStyle(
color: Color.fromARGB(255, 0, 53, 70)),
)),
IconButton(
onPressed: _showSortDialog,
icon: SvgPicture.asset(
'lib/assets/icons/sort2.svg')),
],
),
],
),
),
if (state.studios.length > 1)
ListView.builder(
itemCount: state.studios.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final podcast = state.studios[index];
return PodcastListCard(
podcast: podcast,
onTap: () => _navigateToDetails(index),
);
},
),
],
),
);
},
onRetry: () => context.read<PodcastsState>().getStudios(page: 1),

View File

@ -2,9 +2,12 @@
import 'package:chewie/chewie.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/constants/assets.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/studio_details_data.dart';
import 'package:didvan/providers/user.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/views/comments/comments.dart';
import 'package:didvan/views/comments/comments_state.dart';
@ -32,15 +35,47 @@ class VideoDetailsPage extends StatefulWidget {
State<VideoDetailsPage> createState() => _VideoDetailsPageState();
}
class _VideoDetailsPageState extends State<VideoDetailsPage> {
class _VideoDetailsPageState extends State<VideoDetailsPage>
with TickerProviderStateMixin {
int _currentlyPlayingId = 0;
VideoPlayerController? _videoPlayerController;
ChewieController? _chewieController;
bool _isDescriptionExpanded = false;
final _focusNode = FocusNode();
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
final GlobalKey<AnimatedListState> _relatedContentKey =
GlobalKey<AnimatedListState>();
final GlobalKey<AnimatedListState> _commentsKey =
GlobalKey<AnimatedListState>();
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeIn,
),
);
_slideAnimation =
Tween<Offset>(begin: const Offset(0, 0.2), end: Offset.zero).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeOut,
),
);
final state = context.read<StudioDetailsState>();
state.args = widget.pageData['args'];
@ -52,6 +87,7 @@ class _VideoDetailsPageState extends State<VideoDetailsPage> {
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) {
state.getRelatedContents();
_animationController.forward();
}
});
}
@ -167,48 +203,59 @@ class _VideoDetailsPageState extends State<VideoDetailsPage> {
),
)),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 16 / 9,
child: Stack(
children: [
(_chewieController != null &&
_chewieController!.videoPlayerController
.value.isInitialized)
? Chewie(controller: _chewieController!)
: Center(
child: Image.asset(
Assets.loadingAnimation,
width: 100,
height: 100,
child: FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Hero(
tag: 'video-${state.studio.id}',
child: AspectRatio(
aspectRatio: 16 / 9,
child: Stack(
children: [
(_chewieController != null &&
_chewieController!
.videoPlayerController
.value
.isInitialized)
? Chewie(controller: _chewieController!)
: Center(
child: Image.asset(
Assets.loadingAnimation,
width: 100,
height: 100,
),
),
Positioned(
top: 1,
left: 1,
child: BookmarkButton(
value: state.studio.marked,
onMarkChanged: (value) {
if (widget.pageData['onMarkChanged'] !=
null) {
widget.pageData['onMarkChanged'](
state.studio.id, value);
}
},
gestureSize: 35,
type: 'video',
itemId: state.studio.id,
),
),
Positioned(
top: 1,
left: 1,
child: BookmarkButton(
value: state.studio.marked,
onMarkChanged: (value) {
if (widget.pageData['onMarkChanged'] !=
null) {
widget.pageData['onMarkChanged'](
state.studio.id, value);
}
},
gestureSize: 35,
type: 'video',
itemId: state.studio.id,
],
),
),
],
),
),
_buildDescriptionSection(state),
_buildRelatedContentSection(state),
_buildCommentsSection(state),
],
),
_buildDescriptionSection(state),
_buildRelatedContentSection(state),
_buildCommentsSection(state),
],
),
),
),
),
@ -220,180 +267,236 @@ class _VideoDetailsPageState extends State<VideoDetailsPage> {
}
Widget _buildDescriptionSection(StudioDetailsState state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
state.studio.title,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 0, 53, 70)),
),
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: _isDescriptionExpanded ? double.infinity : 100.0,
return AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
child: KeyedSubtree(
key: ValueKey('description-${state.studio.id}'),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
state.studio.title,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 0, 53, 70)),
),
),
child: Html(
key: ValueKey(state.studio.id),
data: state.studio.description,
onAnchorTap: (href, _, __) => launchUrlString(href!),
style: {
'*': Style(
direction: TextDirection.rtl,
textAlign: TextAlign.right,
lineHeight: LineHeight.percent(135),
margin: const Margins(),
padding: HtmlPaddings.zero,
color: const Color.fromARGB(255, 102, 102, 102),
fontWeight: FontWeight.normal,
),
},
),
),
),
InkWell(
onTap: () {
setState(() {
_isDescriptionExpanded = !_isDescriptionExpanded;
});
},
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
Stack(
alignment: Alignment.bottomCenter,
children: [
SvgPicture.asset(
_isDescriptionExpanded
? 'lib/assets/icons/arrow-up2.svg'
: 'lib/assets/icons/arrow-down.svg',
color: Theme.of(context).primaryColor,
height: 20,
AnimatedSize(
duration: const Duration(milliseconds: 300),
child: ClipRRect(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(
_isDescriptionExpanded ? 0 : 16.0),
),
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: _isDescriptionExpanded
? double.infinity
: 100.0,
),
child: Html(
key: ValueKey(state.studio.id),
data: state.studio.description,
onAnchorTap: (href, _, __) =>
launchUrlString(href!),
style: {
'*': Style(
direction: TextDirection.rtl,
textAlign: TextAlign.right,
lineHeight: LineHeight.percent(135),
margin: const Margins(),
padding: HtmlPaddings.zero,
color: const Color.fromARGB(
255, 102, 102, 102),
fontWeight: FontWeight.normal,
),
},
),
),
),
),
const SizedBox(width: 4),
Text(
_isDescriptionExpanded ? 'کمتر' : 'بیشتر',
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.normal,
Positioned(
bottom: 0,
left: 0,
right: 0,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 300),
opacity: _isDescriptionExpanded ? 0.0 : 1.0,
child: Container(
height: 40,
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(16.0),
),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Theme.of(context)
.colorScheme
.surface
.withOpacity(0.0),
Theme.of(context).colorScheme.surface,
],
stops: const [0.0, 0.9],
),
),
),
),
),
],
),
),
),
if (state.studio.tags.isNotEmpty) const SizedBox(height: 16),
if (state.studio.tags.isNotEmpty)
Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (var i = 0; i < state.studio.tags.length; i++)
TagItem(
tag: state.studio.tags[i],
onMarkChanged: (id, value) {
if (widget.pageData['onMarkChanged'] != null) {
widget.pageData['onMarkChanged'](id, value);
}
},
type: 'video',
InkWell(
onTap: () {
setState(() {
_isDescriptionExpanded = !_isDescriptionExpanded;
});
},
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
_isDescriptionExpanded
? 'lib/assets/icons/arrow-up2.svg'
: 'lib/assets/icons/arrow-down.svg',
color: Theme.of(context).primaryColor,
height: 20,
),
],
),
],
),
],
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SizedBox(),
if (state.nextStudio != null &&
state.alongSideState == AppState.idle)
StudioPreview(
isNext: true,
studio: state.nextStudio!,
),
),
if (state.studio.tags.isNotEmpty) const SizedBox(height: 16),
if (state.studio.tags.isNotEmpty)
Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (var i = 0; i < state.studio.tags.length; i++)
TagItem(
tag: state.studio.tags[i],
onMarkChanged: (id, value) {
if (widget.pageData['onMarkChanged'] != null) {
widget.pageData['onMarkChanged'](id, value);
}
},
type: 'video',
),
],
),
],
),
if (state.alongSideState == AppState.busy)
StudioPreview.placeHolder,
if (state.prevStudio != null &&
state.alongSideState == AppState.idle)
StudioPreview(
isNext: false,
studio: state.prevStudio!,
),
if (state.alongSideState == AppState.busy)
StudioPreview.placeHolder,
const SizedBox(),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SizedBox(),
if (state.nextStudio != null &&
state.alongSideState == AppState.idle)
StudioPreview(
isNext: true,
studio: state.nextStudio!,
),
if (state.alongSideState == AppState.busy)
StudioPreview.placeHolder,
if (state.prevStudio != null &&
state.alongSideState == AppState.idle)
StudioPreview(
isNext: false,
studio: state.prevStudio!,
),
if (state.alongSideState == AppState.busy)
StudioPreview.placeHolder,
const SizedBox(),
],
),
],
),
],
),
);
}
Widget _buildCommentsSection(StudioDetailsState state) {
return Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Color(0xFF059669),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: const DidvanText(
'نظرات',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
SizedBox(
height: 400,
child: ChangeNotifierProvider<CommentsState>(
create: (context) => CommentsState(),
child: Comments(
pageData: {
'id': state.studio.id,
'type': 'studio',
'title': state.studio.title,
'onCommentsChanged': state.onCommentsChanged,
'isPage': false,
return ChangeNotifierProvider<CommentsState>(
create: (context) => CommentsState()
..itemId = state.studio.id
..type = 'studio'
..onCommentsChanged = state.onCommentsChanged
..getComments(),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Consumer<UserProvider>(
builder: (context, userProvider, child) {
final user = userProvider.user;
final hasProfileImage =
user?.photo != null && user!.photo!.isNotEmpty;
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 20,
backgroundColor: Colors.white,
backgroundImage:
hasProfileImage ? NetworkImage(user.photo!) : null,
child: !hasProfileImage
? const Icon(
DidvanIcons.avatar_light,
size: 50,
color: Colors.black,
)
: null,
),
const SizedBox(width: 8),
Expanded(
child: CommentMessageBox(focusNode: _focusNode),
),
],
);
},
),
),
const SizedBox(height: 16),
const SizedBox(
width: double.infinity,
child: DidvanText(
'نظرات کاربران:',
style: TextStyle(
color: Color.fromARGB(255, 0, 53, 70),
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 16),
SizedBox(
height: 400,
child: Comments(
key: _commentsKey,
pageData: const {'isPage': false},
),
),
],
),
],
),
),
);
}
@ -415,7 +518,7 @@ class _VideoDetailsPageState extends State<VideoDetailsPage> {
child: Text(
"مطالب مرتبط:",
style: TextStyle(
fontSize: 16,
fontSize: 18,
color: Color.fromARGB(255, 0, 53, 70),
fontWeight: FontWeight.bold),
),
@ -424,37 +527,81 @@ class _VideoDetailsPageState extends State<VideoDetailsPage> {
padding: const EdgeInsets.all(8),
child: Builder(
builder: (context) {
debugPrint("Building related content section:");
debugPrint(
" - relatedContents.length: ${state.studio.relatedContents.length}");
debugPrint(
" - relatedContentsIsEmpty: ${state.studio.relatedContentsIsEmpty}");
debugPrint(" - tags.length: ${state.studio.tags.length}");
if (state.studio.relatedContents.isNotEmpty) {
debugPrint(
" - Showing ${state.studio.relatedContents.length} related items");
return Column(
children: [
...state.studio.relatedContents
.map((item) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: MultitypeOverview(
item: item,
onMarkChanged: (id, value) {
if (widget.pageData['onMarkChanged'] !=
null) {
widget.pageData['onMarkChanged'](
id, value);
}
},
),
))
.toList(),
],
return AnimatedList(
key: _relatedContentKey,
initialItemCount: state.studio.relatedContents.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index, animation) {
final item = state.studio.relatedContents[index];
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.2),
end: Offset.zero,
).animate(animation),
child: Padding(
padding: const EdgeInsets.only(bottom: 8),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
String routeName;
Map<String, dynamic> arguments;
if (item.type == 'video') {
routeName = Routes.videoDetails;
arguments = {
'id': item.id,
'type': item.type,
};
} else if (item.type == 'podcast') {
routeName = Routes.studioDetails;
arguments = {
'id': item.id,
'type': item.type,
};
} else if (item.type == 'news') {
routeName = Routes.newsDetails;
arguments = {
'id': item.id,
};
} else if (item.type == 'radar') {
routeName = Routes.radarDetails;
arguments = {
'id': item.id,
};
} else {
routeName = Routes.studioDetails;
arguments = {
'id': item.id,
'type': item.type,
};
}
Navigator.pushNamed(
context,
routeName,
arguments: arguments,
);
},
child: MultitypeOverview(
item: item,
onMarkChanged: (id, value) {
if (widget.pageData['onMarkChanged'] !=
null) {
widget.pageData['onMarkChanged'](id, value);
}
},
),
),
),
),
);
},
);
} else if (state.studio.relatedContentsIsEmpty) {
debugPrint(" - Showing empty message");
return const Padding(
padding: EdgeInsets.all(32.0),
child: Center(
@ -468,7 +615,6 @@ class _VideoDetailsPageState extends State<VideoDetailsPage> {
),
);
} else {
debugPrint(" - Showing placeholders (loading state)");
return Column(
children: [
const Padding(
@ -500,8 +646,10 @@ class _VideoDetailsPageState extends State<VideoDetailsPage> {
@override
void dispose() {
_animationController.dispose();
_videoPlayerController?.dispose();
_chewieController?.dispose();
_focusNode.dispose();
super.dispose();
}
}

View File

@ -0,0 +1,132 @@
import 'package:flutter/material.dart';
import 'dart:math' as math;
class AudioWaveformProgress extends StatelessWidget {
final double progress;
final bool isActive;
final ValueChanged<double>? onChanged;
const AudioWaveformProgress({
super.key,
required this.progress,
this.isActive = true,
this.onChanged,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: onChanged != null
? (details) {
final box = context.findRenderObject() as RenderBox;
final localPosition = details.localPosition;
final value = localPosition.dx / box.size.width;
onChanged?.call(value.clamp(0.0, 1.0));
}
: null,
onHorizontalDragUpdate: onChanged != null
? (details) {
final box = context.findRenderObject() as RenderBox;
final localPosition = details.localPosition;
final value = localPosition.dx / box.size.width;
onChanged?.call(value.clamp(0.0, 1.0));
}
: null,
child: Container(
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
child: CustomPaint(
painter: WaveformPainter(
progress: progress.clamp(0.0, 1.0),
isActive: isActive,
),
size: Size.infinite,
),
),
);
}
}
class WaveformPainter extends CustomPainter {
final double progress;
final bool isActive;
WaveformPainter({
required this.progress,
required this.isActive,
});
@override
void paint(Canvas canvas, Size size) {
final random = math.Random(42); // استفاده از seed ثابت برای consistency
const barCount = 60; // تعداد میلهها
final barWidth = (size.width / barCount) * 0.45; // عرض هر میله
final spacing = size.width / barCount;
final inactivePaint = Paint()
..color = const Color.fromARGB(255, 102, 102, 102)
..strokeWidth = barWidth
..strokeCap = StrokeCap.round;
final activePaint = Paint()
..color = const Color.fromARGB(255, 0, 126, 167)
..strokeWidth = barWidth
..strokeCap = StrokeCap.round;
// رسم میلههای موج صدا
for (int i = 0; i < barCount; i++) {
final x = i * spacing + spacing / 2;
// ارتفاع تصادفی برای هر میله (شبیهسازی موج صدا)
final randomHeight = random.nextDouble();
final minHeight = size.height * 0.2;
final maxHeight = size.height * 0.9;
final barHeight = minHeight + (randomHeight * (maxHeight - minHeight));
final y1 = (size.height - barHeight) / 2;
final y2 = y1 + barHeight;
// تعیین رنگ بر اساس progress
final barProgress = i / barCount;
final paint = barProgress <= progress && isActive ? activePaint : inactivePaint;
canvas.drawLine(
Offset(x, y1),
Offset(x, y2),
paint,
);
}
// // رسم thumb (دایره کوچک) در محل progress
// if (isActive && progress > 0) {
// final thumbX = progress * size.width;
// final thumbPaint = Paint()
// ..color = const Color(0xFF3B82F6)
// ..style = PaintingStyle.fill;
// canvas.drawCircle(
// Offset(thumbX, size.height / 2),
// 6,
// thumbPaint,
// );
// // سایه برای thumb
// final shadowPaint = Paint()
// ..color = const Color(0xFF3B82F6).withOpacity(0.3)
// ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4);
// canvas.drawCircle(
// Offset(thumbX, size.height / 2),
// 8,
// shadowPaint,
// );
// }
}
@override
bool shouldRepaint(WaveformPainter oldDelegate) {
return oldDelegate.progress != progress || oldDelegate.isActive != isActive;
}
}

View File

@ -0,0 +1,376 @@
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/models/studio_details_data.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/views/home/media/widgets/audio_waveform_progress.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class FeaturedPodcastCard extends StatefulWidget {
final OverviewData podcast;
final VoidCallback onTap;
final Function(bool)? onPlayStateChanged;
const FeaturedPodcastCard({
super.key,
required this.podcast,
required this.onTap,
this.onPlayStateChanged,
});
@override
State<FeaturedPodcastCard> createState() => _FeaturedPodcastCardState();
}
class _FeaturedPodcastCardState extends State<FeaturedPodcastCard>
with WidgetsBindingObserver {
bool _isPlaying = false;
bool _isLoading = false;
double _progress = 0.0;
Duration _currentPosition = Duration.zero;
Duration _totalDuration = Duration.zero;
var _subscriptions = <dynamic>[];
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
if (widget.podcast.duration != null && widget.podcast.duration! > 0) {
_totalDuration = Duration(seconds: widget.podcast.duration!);
}
_checkCurrentPlayingState();
_setupAudioListener();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.paused ||
state == AppLifecycleState.inactive) {
if (_isPlaying && MediaService.currentPodcast?.id == widget.podcast.id) {
MediaService.audioPlayer.pause();
}
}
}
void _checkCurrentPlayingState() {
if (MediaService.currentPodcast?.id == widget.podcast.id) {
setState(() {
_isPlaying = MediaService.audioPlayer.playing;
});
}
}
void _setupAudioListener() {
_subscriptions
.add(MediaService.audioPlayer.positionStream.listen((position) {
if (mounted && MediaService.currentPodcast?.id == widget.podcast.id) {
setState(() {
_currentPosition = position;
if (_totalDuration.inSeconds > 0) {
_progress = position.inSeconds / _totalDuration.inSeconds;
}
});
}
}));
_subscriptions
.add(MediaService.audioPlayer.durationStream.listen((duration) {
if (mounted &&
duration != null &&
MediaService.currentPodcast?.id == widget.podcast.id) {
setState(() {
_totalDuration = duration;
});
}
}));
_subscriptions.add(MediaService.audioPlayer.playingStream.listen((playing) {
if (mounted && MediaService.currentPodcast?.id == widget.podcast.id) {
setState(() {
_isPlaying = playing;
});
widget.onPlayStateChanged?.call(playing);
}
}));
}
Future<void> _togglePlayPause() async {
if (_isLoading) return;
try {
final isCurrentPodcast =
MediaService.currentPodcast?.id == widget.podcast.id;
if (!isCurrentPodcast) {
setState(() {
_isLoading = true;
});
if (widget.podcast.link == null || widget.podcast.link!.isEmpty) {
throw Exception('لینک پادکست معتبر نیست');
}
MediaService.currentPodcast = StudioDetailsData(
id: widget.podcast.id,
title: widget.podcast.title,
description: widget.podcast.description,
image: widget.podcast.image,
link: widget.podcast.link ?? '',
type: widget.podcast.type,
duration: widget.podcast.duration ?? 0,
iframe: widget.podcast.iframe,
createdAt: widget.podcast.createdAt,
order: 0,
marked: false,
comments: 0,
tags: [],
);
MediaService.isPlayingFromFeaturedCard = true;
await MediaService.handleAudioPlayback(
audioSource: widget.podcast.link!,
id: widget.podcast.id,
isVoiceMessage: false,
isNetworkAudio: true,
);
if (mounted) {
setState(() {
_isPlaying = true;
_isLoading = false;
});
widget.onPlayStateChanged?.call(true);
}
} else {
if (_isPlaying) {
await MediaService.audioPlayer.pause();
if (mounted) {
setState(() {
_isPlaying = false;
});
widget.onPlayStateChanged?.call(false);
}
} else {
await MediaService.audioPlayer.play();
if (mounted) {
setState(() {
_isPlaying = true;
});
widget.onPlayStateChanged?.call(true);
}
}
}
} catch (e) {
debugPrint('Error playing podcast: $e');
if (mounted) {
setState(() {
_isLoading = false;
_isPlaying = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('خطا در پخش پادکست: ${e.toString()}'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
);
}
}
}
Future<void> _seekForward() async {
final newPosition = _currentPosition + const Duration(seconds: 10);
if (newPosition < _totalDuration) {
await MediaService.audioPlayer.seek(newPosition);
}
}
Future<void> _seekBackward() async {
final newPosition = _currentPosition - const Duration(seconds: 5);
if (newPosition > Duration.zero) {
await MediaService.audioPlayer.seek(newPosition);
} else {
await MediaService.audioPlayer.seek(Duration.zero);
}
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final minutes = twoDigits(duration.inMinutes.remainder(60));
final seconds = twoDigits(duration.inSeconds.remainder(60));
return '$minutes:$seconds';
}
@override
Widget build(BuildContext context) {
final bool isCurrentlyPlaying =
MediaService.currentPodcast?.id == widget.podcast.id;
return Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFEBEBEB),
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 2),
child: GestureDetector(
onTap: widget.onTap,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: SkeletonImage(
imageUrl: widget.podcast.image,
width: double.infinity,
height: 200,
borderRadius:
const BorderRadius.vertical(top: Radius.circular(16)),
),
),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
DidvanText(
widget.podcast.title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 16),
Row(
children: [
SizedBox(
width: 45,
child: Text(
_formatDuration(_totalDuration),
style: const TextStyle(
fontSize: 12,
color: Color.fromARGB(255, 102, 102, 102)),
textAlign: TextAlign.left,
),
),
const SizedBox(width: 8),
Expanded(
child: AudioWaveformProgress(
progress: isCurrentlyPlaying
? _progress.clamp(0.0, 1.0)
: 0.0,
isActive: isCurrentlyPlaying,
onChanged: isCurrentlyPlaying
? (value) {
final newPosition = Duration(
seconds: (value * _totalDuration.inSeconds)
.toInt(),
);
MediaService.audioPlayer.seek(newPosition);
}
: null,
),
),
const SizedBox(width: 8),
SizedBox(
width: 45,
child: Text(
_formatDuration(_currentPosition),
style: const TextStyle(
fontSize: 12,
color: Color.fromARGB(255, 102, 102, 102)),
textAlign: TextAlign.right,
),
),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: isCurrentlyPlaying ? _seekForward : null,
icon: SvgPicture.asset(
'lib/assets/icons/forward-10-seconds.svg',
width: 30,
height: 30,
),
),
const SizedBox(width: 15),
GestureDetector(
onTap: _togglePlayPause,
child: _isLoading
? const SizedBox(
width: 50,
height: 50,
child: Center(
child: CircularProgressIndicator(
color: Color(0xFF3B82F6),
strokeWidth: 3,
),
),
)
: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return ScaleTransition(
scale: animation,
child: child,
);
},
child: SvgPicture.asset(
isCurrentlyPlaying && _isPlaying
? 'lib/assets/icons/pause-circle.svg'
: 'lib/assets/icons/play.svg',
key: ValueKey<bool>(
isCurrentlyPlaying && _isPlaying),
width:
isCurrentlyPlaying && _isPlaying ? 55 : 50,
height:
isCurrentlyPlaying && _isPlaying ? 55 : 50,
),
),
),
const SizedBox(width: 15),
IconButton(
onPressed: isCurrentlyPlaying ? _seekBackward : null,
icon: SvgPicture.asset(
'lib/assets/icons/backward-5-seconds.svg',
width: 30,
height: 30,
),
),
],
),
],
),
),
],
),
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
for (var subscription in _subscriptions) {
subscription.cancel();
}
_subscriptions.clear();
super.dispose();
}
}

View File

@ -0,0 +1,138 @@
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
class PodcastListCard extends StatelessWidget {
final OverviewData podcast;
final VoidCallback onTap;
const PodcastListCard({
super.key,
required this.podcast,
required this.onTap,
});
String _formatDuration(int? duration) {
if (duration == null) return '';
final minutes = duration ~/ 60;
final seconds = duration % 60;
return '${seconds.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}';
}
String _formatDate(String dateStr) {
try {
final date = DateTime.parse(dateStr);
return date.toPersianDateStr();
} catch (e) {
return dateStr;
}
}
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return GestureDetector(
onTap: onTap,
child: Column(
children: [
Container(
height: 120,
margin: const EdgeInsets.only(
bottom: 8,
left: 16,
right: 16,
),
decoration: BoxDecoration(
color: const Color.fromRGBO(235, 255, 255, 255),
borderRadius: BorderRadius.circular(20),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(6.0),
child: AspectRatio(
aspectRatio: 1.42,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: SkeletonImage(
imageUrl: podcast.image,
width: double.infinity,
height: double.infinity,
borderRadius: BorderRadius.circular(16),
),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 3, 12, 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
DidvanText(
podcast.title,
style: textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: Colors.black87,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Spacer(),
Row(
children: [
SvgPicture.asset('lib/assets/icons/calendar.svg'),
const SizedBox(width: 4),
DidvanText(
_formatDate(podcast.createdAt),
style: textTheme.bodySmall?.copyWith(
color: const Color.fromARGB(255, 102, 102, 102),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
const SizedBox(height: 7),
Row(
children: [
SvgPicture.asset(
'lib/assets/icons/clock.svg',
color: const Color.fromARGB(255, 102, 102, 102),
),
const SizedBox(width: 4),
DidvanText(
_formatDuration(podcast.duration).toPersianDigit(),
style: textTheme.bodySmall?.copyWith(
color: const Color.fromARGB(255, 102, 102, 102),
),
),
],
),
],
),
),
),
],
),
),
),
Divider(
color: Colors.grey[300],
thickness: 2,
indent: 16,
endIndent: 16,
),
],
),
);
}
}

View File

@ -157,7 +157,7 @@ class _StudioDetailsState extends State<StudioDetails> {
value: state.studio.marked,
onMarkChanged: (value) {
widget.pageData['onMarkChanged'](
state.studio.id, value);
state.studio.id, value, true);
},
gestureSize: 48,
),

View File

@ -40,71 +40,115 @@ class _PlayerNavBarState extends State<PlayerNavBar> {
Widget build(BuildContext context) {
return StreamBuilder<bool>(
stream: MediaService.audioPlayer.playingStream,
builder: (context, isPlaying) => GestureDetector(
onTap: () {
(MediaService.currentPodcast == null &&
(MediaService.audioPlayerTag ?? '')
.split('-')[1]
.isNotEmpty) ||
MediaService.currentPodcast?.description == 'radar'
? Navigator.of(context).pushNamed(
Routes.radarDetails,
arguments: {
'onMarkChanged': (id, value) {},
'onCommentsChanged': (id, value) {},
'id': MediaService.currentPodcast?.id,
'args': const RadarRequestArgs(page: 0),
'hasUnmarkConfirmation': false,
},
)
: (MediaService.audioPlayerTag ?? '').split('-')[1].isNotEmpty
? _showPlayerBottomSheet(context)
: null;
},
child: Consumer<StudioDetailsState>(
builder: (context, state, child) => AnimatedContainer(
height: widget.inHome
? _enablePlayerController(state)
? 72 + 32 + 32
: 72
: null,
duration: DesignConfig.lowAnimationDuration,
decoration: BoxDecoration(
color: DesignConfig.isDark
? Theme.of(context).colorScheme.focused
: Theme.of(context).colorScheme.navigation,
borderRadius: BorderRadius.vertical(
top: const Radius.circular(16),
bottom:
widget.inHome ? Radius.zero : const Radius.circular(16)),
),
alignment: widget.inHome ? Alignment.topCenter : Alignment.center,
child: Builder(builder: (context) {
if (!_enablePlayerController(state)) {
return const SizedBox();
}
if (state.appState == AppState.failed) {
Future.delayed(const Duration(seconds: 2), () {
MediaService.resetAudioPlayer();
state.update();
// _enablePlayerController(state);
});
return Padding(
padding: const EdgeInsets.only(top: 18.0),
child: DidvanText(
'اتصال اینترنت برقرار نمی‌باشد',
color: DesignConfig.isDark
? Theme.of(context).colorScheme.title
: Theme.of(context).colorScheme.secondCTA,
),
);
}
if (MediaService.currentPodcast == null) {
builder: (context, isPlaying) {
return GestureDetector(
onTap: () {
if (MediaService.isPlayingFromFeaturedCard) {
return;
}
(MediaService.currentPodcast == null &&
(MediaService.audioPlayerTag ?? '')
.split('-')[1]
.isNotEmpty) ||
MediaService.currentPodcast?.description == 'radar'
? Navigator.of(context).pushNamed(
Routes.radarDetails,
arguments: {
'onMarkChanged': (id, value) {},
'onCommentsChanged': (id, value) {},
'id': MediaService.currentPodcast?.id,
'args': const RadarRequestArgs(page: 0),
'hasUnmarkConfirmation': false,
},
)
: (MediaService.audioPlayerTag ?? '').split('-')[1].isNotEmpty
? _showPlayerBottomSheet(context)
: null;
},
child: Consumer<StudioDetailsState>(
builder: (context, state, child) => AnimatedContainer(
height: widget.inHome
? _enablePlayerController(state) &&
!MediaService.isPlayingFromFeaturedCard
? 72 + 32 + 32
: 72
: null,
duration: DesignConfig.lowAnimationDuration,
decoration: BoxDecoration(
color: DesignConfig.isDark
? Theme.of(context).colorScheme.focused
: Theme.of(context).colorScheme.navigation,
borderRadius: BorderRadius.vertical(
top: const Radius.circular(16),
bottom: widget.inHome
? Radius.zero
: const Radius.circular(16)),
),
alignment: widget.inHome ? Alignment.topCenter : Alignment.center,
child: Builder(builder: (context) {
if (MediaService.isPlayingFromFeaturedCard) {
return const SizedBox();
}
if (!_enablePlayerController(state)) {
return const SizedBox();
}
if (state.appState == AppState.failed) {
Future.delayed(const Duration(seconds: 2), () {
MediaService.resetAudioPlayer();
state.update();
// _enablePlayerController(state);
});
return Padding(
padding: const EdgeInsets.only(top: 18.0),
child: DidvanText(
'اتصال اینترنت برقرار نمی‌باشد',
color: DesignConfig.isDark
? Theme.of(context).colorScheme.title
: Theme.of(context).colorScheme.secondCTA,
),
);
}
if (MediaService.currentPodcast == null) {
return Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 16)
.copyWith(bottom: widget.inHome ? 72 + 16 : 16),
child: Row(
children: [
DidvanIconButton(
icon: DidvanIcons.close_regular,
color: DesignConfig.isDark
? null
: Theme.of(context).colorScheme.secondCTA,
gestureSize: 28,
onPressed: () async {
await MediaService.resetAudioPlayer();
},
),
Expanded(
child: Center(
child: Padding(
padding: const EdgeInsets.only(left: 48),
child: SpinKitThreeBounce(
size: 18,
color: DesignConfig.isDark
? Theme.of(context).colorScheme.title
: Theme.of(context).colorScheme.secondCTA,
),
),
),
),
],
),
);
}
return Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 16)
.copyWith(bottom: widget.inHome ? 72 + 16 : 16),
const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanIconButton(
icon: DidvanIcons.close_regular,
@ -114,137 +158,107 @@ class _PlayerNavBarState extends State<PlayerNavBar> {
gestureSize: 28,
onPressed: () async {
await MediaService.resetAudioPlayer();
// _enablePlayerController(state);
// state.update();
},
),
const SizedBox(width: 8),
SkeletonImage(
imageUrl: MediaService.currentPodcast!.image,
width: 32,
height: 32,
),
const SizedBox(width: 8),
Expanded(
child: Center(
child: Padding(
padding: const EdgeInsets.only(left: 48),
child: SpinKitThreeBounce(
size: 18,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(
MediaService.currentPodcast!.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
color: DesignConfig.isDark
? Theme.of(context).colorScheme.title
? null
: Theme.of(context).colorScheme.secondCTA,
),
),
AudioSlider(
disableThumb: true,
tag: MediaService.audioPlayerTag!,
),
],
),
),
const SizedBox(
width: 8,
),
// StreamBuilder<PlayingAudio?>(
// stream: MediaService.audioPlayer.onReadyToPlay,
// builder: (context, snapshot) {
// if (snapshot.data == null ||
// state.appState == AppState.busy &&
// MediaService.currentPodcast?.description !=
// 'radar') {
// return SizedBox(
// height: 18,
// width: 18,
// child: CircularProgressIndicator(
// strokeWidth: 2,
// color: DesignConfig.isDark
// ? Theme.of(context).colorScheme.title
// : Theme.of(context).colorScheme.secondCTA,
// ),
// );
// }
// return const SizedBox();
// },
// ),
(state.appState != AppState.busy &&
isPlaying.data != null ||
MediaService.currentPodcast?.description ==
'radar')
? DidvanIconButton(
gestureSize: 28,
color: DesignConfig.isDark
? null
: Theme.of(context).colorScheme.secondCTA,
icon: isPlaying.data!
? DidvanIcons.pause_solid
: DidvanIcons.play_solid,
onPressed: () {
if (state.args?.type == 'video') {
state.getStudioDetails(
MediaService.currentPodcast!.id,
args: state.podcastArgs,
fetchOnly: true,
);
}
MediaService.handleAudioPlayback(
audioSource:
MediaService.currentPodcast!.link,
id: MediaService.currentPodcast!.id,
isVoiceMessage: false,
);
},
)
: SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: DesignConfig.isDark
? Theme.of(context).colorScheme.title
: Theme.of(context).colorScheme.secondCTA,
),
)
],
),
);
}
return Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanIconButton(
icon: DidvanIcons.close_regular,
color: DesignConfig.isDark
? null
: Theme.of(context).colorScheme.secondCTA,
gestureSize: 28,
onPressed: () async {
await MediaService.resetAudioPlayer();
// _enablePlayerController(state);
// state.update();
},
),
const SizedBox(width: 8),
SkeletonImage(
imageUrl: MediaService.currentPodcast!.image,
width: 32,
height: 32,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(
MediaService.currentPodcast!.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
color: DesignConfig.isDark
? null
: Theme.of(context).colorScheme.secondCTA,
),
AudioSlider(
disableThumb: true,
tag: MediaService.audioPlayerTag!,
),
],
),
),
const SizedBox(
width: 8,
),
// StreamBuilder<PlayingAudio?>(
// stream: MediaService.audioPlayer.onReadyToPlay,
// builder: (context, snapshot) {
// if (snapshot.data == null ||
// state.appState == AppState.busy &&
// MediaService.currentPodcast?.description !=
// 'radar') {
// return SizedBox(
// height: 18,
// width: 18,
// child: CircularProgressIndicator(
// strokeWidth: 2,
// color: DesignConfig.isDark
// ? Theme.of(context).colorScheme.title
// : Theme.of(context).colorScheme.secondCTA,
// ),
// );
// }
// return const SizedBox();
// },
// ),
(state.appState != AppState.busy &&
isPlaying.data != null ||
MediaService.currentPodcast?.description == 'radar')
? DidvanIconButton(
gestureSize: 28,
color: DesignConfig.isDark
? null
: Theme.of(context).colorScheme.secondCTA,
icon: isPlaying.data!
? DidvanIcons.pause_solid
: DidvanIcons.play_solid,
onPressed: () {
if (state.args?.type == 'video') {
state.getStudioDetails(
MediaService.currentPodcast!.id,
args: state.podcastArgs,
fetchOnly: true,
);
}
MediaService.handleAudioPlayback(
audioSource: MediaService.currentPodcast!.link,
id: MediaService.currentPodcast!.id,
isVoiceMessage: false,
);
},
)
: SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: DesignConfig.isDark
? Theme.of(context).colorScheme.title
: Theme.of(context).colorScheme.secondCTA,
),
)
],
),
);
}),
}),
),
),
),
),
);
},
);
}

View File

@ -1,12 +1,10 @@
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/models/requests/news.dart';
import 'package:didvan/models/requests/radar.dart';
import 'package:didvan/models/requests/studio.dart';
import 'package:didvan/providers/user.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/utils/date_time.dart';
import 'package:didvan/views/widgets/bookmark_button.dart';
import 'package:didvan/views/widgets/didvan/card.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
@ -88,14 +86,15 @@ class MultitypeOverview extends StatelessWidget {
children: [
Stack(
children: [
SkeletonImage(imageUrl: item.image, height: 80, width: 80),
SkeletonImage(imageUrl: item.image, height: 100, width: 140),
Container(
padding:
const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondary,
color: Theme.of(context).colorScheme.primary,
borderRadius: const BorderRadius.horizontal(
left: Radius.circular(20),
left: Radius.circular(16),
right: Radius.circular(16),
),
),
child: SvgPicture.asset(
@ -127,10 +126,7 @@ class MultitypeOverview extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
DidvanIcons.calendar_day_light,
size: 16,
),
SvgPicture.asset('lib/assets/icons/calendar.svg'),
const SizedBox(width: 4),
DidvanText(
DateTime.parse(item.createdAt)
@ -140,26 +136,26 @@ class MultitypeOverview extends StatelessWidget {
],
),
const Spacer(),
if ((item.timeToRead ?? item.duration) != null)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
DidvanIcons.timer_light,
size: 16,
),
const SizedBox(width: 4),
DidvanText(
item.timeToRead != null
? '${item.timeToRead} دقیقه'
: DateTimeUtils.normalizeTimeDuration(
Duration(seconds: item.duration!),
),
style:
Theme.of(context).textTheme.labelSmall,
),
],
),
// if ((item.timeToRead ?? item.duration) != null)
// Row(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// const Icon(
// DidvanIcons.timer_light,
// size: 16,
// ),
// const SizedBox(width: 4),
// DidvanText(
// item.timeToRead != null
// ? '${item.timeToRead} دقیقه'
// : DateTimeUtils.normalizeTimeDuration(
// Duration(seconds: item.duration!),
// ),
// style:
// Theme.of(context).textTheme.labelSmall,
// ),
// ],
// ),
],
),
),