sub init() ' BASE OFICIAL QUE JÁ FUNCIONOU NA SUA ROKU m.apiBase = "https://webapp.lat" m.channelStore = m.top.findNode("channelStore") if m.channelStore <> invalid then m.channelStore.observeField("userData", "onRokuUserDataReceived") end if m.rokuUserDataRequested = false m.rokuUserEmail = "" m.logoUrl = "" m.backgroundUrl = "" m.homeBackgroundUrl = "" m.startupBannerUrl = "" m.homeBannerUrl = "" m.assetsVersion = "" m.appVersionDisplay = "1.8.236" m.appTitleText = "WEBTV" m.appSubtitleText = "Canais, favoritos, filmes e séries em uma experiência moderna" m.startupLoadingText = "Carregando sistema de ativação" m.startupHintText = "Cadastre o código em webapp.lat" m.loginTitleText = "WEBTV" m.loginInfoText = "Digite código do provedor, usuário e senha." ' Textos customizáveis pelo painel - Home / Lista / Login m.homeLiveText = "TV ao vivo" m.homeMoviesText = "Filmes" m.homeSeriesText = "Séries" m.homeSportsText = "Esportes" m.homeListText = "Lista" m.homeSettingsText = "Configurações" m.homeReloadText = "Recarregar" m.homeExpirationPrefix = "Vencimento da lista:" m.listTitleText = "MINHA LISTA" m.listUsernameLabel = "Usuário:" m.listExpirationLabel = "Vencimento:" m.listDeviceLabel = "Chave do dispositivo:" m.listVersionLabel = "Versão do aplicativo:" m.listChangeLoginText = "Trocar lista / Login" m.listDeleteText = "Excluir lista" m.listBackText = "Voltar" m.loginProviderLabelText = "Provedor" m.loginUserLabelText = "Usuário" m.loginPasswordLabelText = "Senha" m.loginButtonTextValue = "ENTRAR" m.loginRefreshTextValue = "ATUALIZAR" m.loginKeyboardDoneText = "OK" m.loginKeyboardClearText = "LIMPAR" m.loginKeyboardSpaceText = "ESPAÇO" m.loginKeyboardBackText = "VOLTAR" m.loadingLogoUrl = "" m.loadingTitleText = "" m.loadingSubText = "" m.loadingStyle = "static" m.startupAnimGroup = m.top.findNode("startupAnimGroup") m.startupAnimNinja = m.top.findNode("startupAnimNinja") m.startupAnimTimer = m.top.findNode("startupAnimTimer") m.startupProceedTimer = m.top.findNode("startupProceedTimer") m.startupNinjaFrames = [ "pkg:/images/webtv_startup_frame_1_v251.png", "pkg:/images/webtv_startup_frame_2_v251.png", "pkg:/images/webtv_startup_frame_3_v251.png", "pkg:/images/webtv_startup_frame_4_v251.png" ] m.startupNinjaFrameIndex = 0 m.pendingStartupToken = "" m.title = m.top.findNode("title") m.loginTitle = m.top.findNode("loginTitle") m.code = m.top.findNode("code") m.status = m.top.findNode("status") m.hint = m.top.findNode("hint") m.footer = m.top.findNode("footer") m.movieLoadingGroup = m.top.findNode("movieLoadingGroup") m.movieLoadingSpinner = m.top.findNode("movieLoadingSpinner") m.movieLoadingLogo = m.top.findNode("movieLoadingLogo") m.movieLoadingTitle = m.top.findNode("movieLoadingTitle") m.movieLoadingSub = m.top.findNode("movieLoadingSub") m.movieLoadingStep = 0 m.movieLoadingTimer = m.top.findNode("movieLoadingTimer") m.loginFocusTimer = m.top.findNode("loginFocusTimer") m.appBackgroundImg = m.top.findNode("appBackgroundImg") m.startupBannerImg = m.top.findNode("startupBannerImg") m.homeBannerImg = m.top.findNode("homeBannerImg") m.loginGroup = m.top.findNode("loginGroup") m.loginDnsLabel = m.top.findNode("loginDnsLabel") m.loginUserLabel = m.top.findNode("loginUserLabel") m.loginPassLabel = m.top.findNode("loginPassLabel") m.loginButtonTextNode = m.top.findNode("loginButtonText") m.loginRefreshButtonTextNode = m.top.findNode("loginRefreshButtonText") m.loginDeviceKeyLabelNode = m.top.findNode("loginDeviceKeyLabel") m.loginVersionLabelNode = m.top.findNode("loginVersionLabel") m.loginDnsValue = m.top.findNode("loginDnsValue") m.loginUserValue = m.top.findNode("loginUserValue") m.loginPassValue = m.top.findNode("loginPassValue") m.loginStatus = m.top.findNode("loginStatus") m.loginDnsBox = m.top.findNode("loginDnsBox") m.loginUserBox = m.top.findNode("loginUserBox") m.loginPassBox = m.top.findNode("loginPassBox") m.loginButtonBox = m.top.findNode("loginButtonBox") m.loginRefreshButtonBox = m.top.findNode("loginRefreshButtonBox") m.listInfoGroup = m.top.findNode("listInfoGroup") m.listInfoTitle = m.top.findNode("listInfoTitle") m.listInfoUser = m.top.findNode("listInfoUser") m.listInfoExpiry = m.top.findNode("listInfoExpiry") m.listInfoDevice = m.top.findNode("listInfoDevice") m.listInfoVersion = m.top.findNode("listInfoVersion") m.listInfoOptionFocus = [m.top.findNode("listInfoOptionFocus1"), m.top.findNode("listInfoOptionFocus2"), m.top.findNode("listInfoOptionFocus3")] m.listInfoOptionBg = [m.top.findNode("listInfoOptionBg1"), m.top.findNode("listInfoOptionBg2"), m.top.findNode("listInfoOptionBg3")] m.listInfoOptionLabel = [m.top.findNode("listInfoOption1"), m.top.findNode("listInfoOption2"), m.top.findNode("listInfoOption3")] m.loginKeyboardGroup = m.top.findNode("loginKeyboardGroup") m.loginKeyboardTitle = m.top.findNode("loginKeyboardTitle") m.loginKeyboardValue = m.top.findNode("loginKeyboardValue") m.loginDynKeyboard = m.top.findNode("loginDynKeyboard") if m.loginDynKeyboard <> invalid then m.loginDynKeyboard.observeField("text", "onLoginDynKeyboardTextChanged") m.loginDynKeyboard.observeField("continue", "onLoginDynKeyboardContinue") end if m.loginKeys = [ m.top.findNode("loginKey1"), m.top.findNode("loginKey2"), m.top.findNode("loginKey3"), m.top.findNode("loginKey4"), m.top.findNode("loginKey5"), m.top.findNode("loginKey6"), m.top.findNode("loginKey7"), m.top.findNode("loginKey8"), m.top.findNode("loginKey9"), m.top.findNode("loginKey10"), m.top.findNode("loginKey11"), m.top.findNode("loginKey12"), m.top.findNode("loginKey13"), m.top.findNode("loginKey14"), m.top.findNode("loginKey15"), m.top.findNode("loginKey16"), m.top.findNode("loginKey17"), m.top.findNode("loginKey18"), m.top.findNode("loginKey19"), m.top.findNode("loginKey20"), m.top.findNode("loginKey21"), m.top.findNode("loginKey22"), m.top.findNode("loginKey23"), m.top.findNode("loginKey24"), m.top.findNode("loginKey25"), m.top.findNode("loginKey26"), m.top.findNode("loginKey27"), m.top.findNode("loginKey28"), m.top.findNode("loginKey29"), m.top.findNode("loginKey30"), m.top.findNode("loginKey31"), m.top.findNode("loginKey32"), m.top.findNode("loginKey33"), m.top.findNode("loginKey34"), m.top.findNode("loginKey35"), m.top.findNode("loginKey36"), m.top.findNode("loginKey37"), m.top.findNode("loginKey38"), m.top.findNode("loginKey39"), m.top.findNode("loginKey40"), m.top.findNode("loginKey41"), m.top.findNode("loginKey42"), m.top.findNode("loginKey43"), m.top.findNode("loginKey44") ] m.kbPanel = m.top.findNode("kbPanel") m.kbTitle = m.top.findNode("kbTitle") m.kbInputBg = m.top.findNode("kbInputBg") m.kbInput = m.top.findNode("kbInput") m.kbKeyBg = [ m.top.findNode("kbKeyBg1"), m.top.findNode("kbKeyBg2"), m.top.findNode("kbKeyBg3"), m.top.findNode("kbKeyBg4"), m.top.findNode("kbKeyBg5"), m.top.findNode("kbKeyBg6"), m.top.findNode("kbKeyBg7"), m.top.findNode("kbKeyBg8"), m.top.findNode("kbKeyBg9"), m.top.findNode("kbKeyBg10"), m.top.findNode("kbKeyBg11"), m.top.findNode("kbKeyBg12"), m.top.findNode("kbKeyBg13"), m.top.findNode("kbKeyBg14"), m.top.findNode("kbKeyBg15"), m.top.findNode("kbKeyBg16"), m.top.findNode("kbKeyBg17"), m.top.findNode("kbKeyBg18"), m.top.findNode("kbKeyBg19"), m.top.findNode("kbKeyBg20"), m.top.findNode("kbKeyBg21"), m.top.findNode("kbKeyBg22"), m.top.findNode("kbKeyBg23"), m.top.findNode("kbKeyBg24"), m.top.findNode("kbKeyBg25"), m.top.findNode("kbKeyBg26"), m.top.findNode("kbKeyBg27"), m.top.findNode("kbKeyBg28"), m.top.findNode("kbKeyBg29"), m.top.findNode("kbKeyBg30"), m.top.findNode("kbKeyBg31"), m.top.findNode("kbKeyBg32"), m.top.findNode("kbKeyBg33"), m.top.findNode("kbKeyBg34"), m.top.findNode("kbKeyBg35"), m.top.findNode("kbKeyBg36"), m.top.findNode("kbKeyBg37"), m.top.findNode("kbKeyBg38"), m.top.findNode("kbKeyBg39"), m.top.findNode("kbKeyBg40"), m.top.findNode("kbKeyBg41"), m.top.findNode("kbKeyBg42") ] m.kbKey = [ m.top.findNode("kbKey1"), m.top.findNode("kbKey2"), m.top.findNode("kbKey3"), m.top.findNode("kbKey4"), m.top.findNode("kbKey5"), m.top.findNode("kbKey6"), m.top.findNode("kbKey7"), m.top.findNode("kbKey8"), m.top.findNode("kbKey9"), m.top.findNode("kbKey10"), m.top.findNode("kbKey11"), m.top.findNode("kbKey12"), m.top.findNode("kbKey13"), m.top.findNode("kbKey14"), m.top.findNode("kbKey15"), m.top.findNode("kbKey16"), m.top.findNode("kbKey17"), m.top.findNode("kbKey18"), m.top.findNode("kbKey19"), m.top.findNode("kbKey20"), m.top.findNode("kbKey21"), m.top.findNode("kbKey22"), m.top.findNode("kbKey23"), m.top.findNode("kbKey24"), m.top.findNode("kbKey25"), m.top.findNode("kbKey26"), m.top.findNode("kbKey27"), m.top.findNode("kbKey28"), m.top.findNode("kbKey29"), m.top.findNode("kbKey30"), m.top.findNode("kbKey31"), m.top.findNode("kbKey32"), m.top.findNode("kbKey33"), m.top.findNode("kbKey34"), m.top.findNode("kbKey35"), m.top.findNode("kbKey36"), m.top.findNode("kbKey37"), m.top.findNode("kbKey38"), m.top.findNode("kbKey39"), m.top.findNode("kbKey40"), m.top.findNode("kbKey41"), m.top.findNode("kbKey42") ] m.menuTitle = m.top.findNode("menuTitle") m.menu1 = m.top.findNode("menu1") m.menu2 = m.top.findNode("menu2") m.menu3 = m.top.findNode("menu3") m.menu4 = m.top.findNode("menu4") m.menu5 = m.top.findNode("menu5") m.menu6 = m.top.findNode("menu6") m.menu7 = m.top.findNode("menu7") m.heroCard = m.top.findNode("heroCard") m.heroTitle = m.top.findNode("heroTitle") m.heroSub = m.top.findNode("heroSub") m.rowTitle = m.top.findNode("rowTitle") m.card1 = m.top.findNode("card1") m.card2 = m.top.findNode("card2") m.card3 = m.top.findNode("card3") m.card4 = m.top.findNode("card4") m.card5 = m.top.findNode("card5") m.card6 = m.top.findNode("card6") m.cardLabel1 = m.top.findNode("cardLabel1") m.cardLabel2 = m.top.findNode("cardLabel2") m.cardLabel3 = m.top.findNode("cardLabel3") m.cardLabel4 = m.top.findNode("cardLabel4") m.cardLabel5 = m.top.findNode("cardLabel5") m.cardLabel6 = m.top.findNode("cardLabel6") m.sportsGroup = m.top.findNode("sportsGroup") m.sportsTitle = m.top.findNode("sportsTitle") m.sportsSubtitle = m.top.findNode("sportsSubtitle") m.sportsCountLabel = m.top.findNode("sportsCountLabel") m.sportsMessage = m.top.findNode("sportsMessage") m.sportsPager = m.top.findNode("sportsPager") m.sportsTabBg = [m.top.findNode("sportsTabBg1"), m.top.findNode("sportsTabBg2"), m.top.findNode("sportsTabBg3")] m.sportsTabLabel = [m.top.findNode("sportsTabLabel1"), m.top.findNode("sportsTabLabel2"), m.top.findNode("sportsTabLabel3")] m.sportsCardBg = [m.top.findNode("sportsCard1"), m.top.findNode("sportsCard2"), m.top.findNode("sportsCard3"), m.top.findNode("sportsCard4"), m.top.findNode("sportsCard5"), m.top.findNode("sportsCard6")] m.sportsLeague = [m.top.findNode("sportsLeague1"), m.top.findNode("sportsLeague2"), m.top.findNode("sportsLeague3"), m.top.findNode("sportsLeague4"), m.top.findNode("sportsLeague5"), m.top.findNode("sportsLeague6")] m.sportsTime = [m.top.findNode("sportsTime1"), m.top.findNode("sportsTime2"), m.top.findNode("sportsTime3"), m.top.findNode("sportsTime4"), m.top.findNode("sportsTime5"), m.top.findNode("sportsTime6")] m.sportsHomeLogo = [m.top.findNode("sportsHomeLogo1"), m.top.findNode("sportsHomeLogo2"), m.top.findNode("sportsHomeLogo3"), m.top.findNode("sportsHomeLogo4"), m.top.findNode("sportsHomeLogo5"), m.top.findNode("sportsHomeLogo6")] m.sportsAwayLogo = [m.top.findNode("sportsAwayLogo1"), m.top.findNode("sportsAwayLogo2"), m.top.findNode("sportsAwayLogo3"), m.top.findNode("sportsAwayLogo4"), m.top.findNode("sportsAwayLogo5"), m.top.findNode("sportsAwayLogo6")] m.sportsHomeName = [m.top.findNode("sportsHomeName1"), m.top.findNode("sportsHomeName2"), m.top.findNode("sportsHomeName3"), m.top.findNode("sportsHomeName4"), m.top.findNode("sportsHomeName5"), m.top.findNode("sportsHomeName6")] m.sportsAwayName = [m.top.findNode("sportsAwayName1"), m.top.findNode("sportsAwayName2"), m.top.findNode("sportsAwayName3"), m.top.findNode("sportsAwayName4"), m.top.findNode("sportsAwayName5"), m.top.findNode("sportsAwayName6")] m.sportsScore = [m.top.findNode("sportsScore1"), m.top.findNode("sportsScore2"), m.top.findNode("sportsScore3"), m.top.findNode("sportsScore4"), m.top.findNode("sportsScore5"), m.top.findNode("sportsScore6")] m.sportsChannel = [m.top.findNode("sportsChannel1"), m.top.findNode("sportsChannel2"), m.top.findNode("sportsChannel3"), m.top.findNode("sportsChannel4"), m.top.findNode("sportsChannel5"), m.top.findNode("sportsChannel6")] m.sportsAllItems = [] m.sportsItems = [] m.sportsPage = 0 m.sportsIndex = 0 m.sportsPerPage = 6 m.sportsTabIndex = 0 m.sportsFocusArea = "cards" m.pendingSportsEvent = invalid m.sportsCacheItems = [] m.sportsCacheReady = false m.sportsPrefetching = false m.sportsChannelCatalog = [] m.sportsCatalogPrefetching = false m.sportsFootballUrl = "" m.sportsUfcUrl = "https://apisport.tropatech.com.br/user/ver_json.php?token=1ae40fad5bba86b304d2e0595b1a22ed&modalidade=ufc" m.sportsBasketUrl = "https://apisport.tropatech.com.br/user/ver_json.php?token=1ae40fad5bba86b304d2e0595b1a22ed&modalidade=basquete" m.homeLogo = m.top.findNode("homeLogo") m.awRedHomeBg = m.top.findNode("awRedHomeBg") m.cardIcon1 = m.top.findNode("cardIcon1") m.cardIcon2 = m.top.findNode("cardIcon2") m.cardIcon3 = m.top.findNode("cardIcon3") m.cardIcon4 = m.top.findNode("cardIcon4") m.cardIcon5 = m.top.findNode("cardIcon5") m.cardIcon6 = m.top.findNode("cardIcon6") m.premiumHint = m.top.findNode("premiumHint") m.reloadBtnBg = m.top.findNode("reloadBtnBg") m.expiryBg = m.top.findNode("expiryBg") m.expiryLabel = m.top.findNode("expiryLabel") m.seriesFavBadgeBg = [ m.top.findNode("seriesFavBadgeBg1"), m.top.findNode("seriesFavBadgeBg2"), m.top.findNode("seriesFavBadgeBg3"), m.top.findNode("seriesFavBadgeBg4"), m.top.findNode("seriesFavBadgeBg5"), m.top.findNode("seriesFavBadgeBg6") ] m.seriesFavBadge = [ m.top.findNode("seriesFavBadge1"), m.top.findNode("seriesFavBadge2"), m.top.findNode("seriesFavBadge3"), m.top.findNode("seriesFavBadge4"), m.top.findNode("seriesFavBadge5"), m.top.findNode("seriesFavBadge6") ] m.homeSeriesContinueTitle = m.top.findNode("homeSeriesContinueTitle") m.homeSerContFocus = [ m.top.findNode("homeSerContFocus1"), m.top.findNode("homeSerContFocus2"), m.top.findNode("homeSerContFocus3"), m.top.findNode("homeSerContFocus4") ] m.homeSerContPoster = [ m.top.findNode("homeSerContPoster1"), m.top.findNode("homeSerContPoster2"), m.top.findNode("homeSerContPoster3"), m.top.findNode("homeSerContPoster4") ] m.homeSerContProgBg = [ m.top.findNode("homeSerContProgBg1"), m.top.findNode("homeSerContProgBg2"), m.top.findNode("homeSerContProgBg3"), m.top.findNode("homeSerContProgBg4") ] m.homeSerContProgFill = [ m.top.findNode("homeSerContProgFill1"), m.top.findNode("homeSerContProgFill2"), m.top.findNode("homeSerContProgFill3"), m.top.findNode("homeSerContProgFill4") ] m.homeSerContLabel = [ m.top.findNode("homeSerContLabel1"), m.top.findNode("homeSerContLabel2"), m.top.findNode("homeSerContLabel3"), m.top.findNode("homeSerContLabel4") ] m.homeSeriesFavTitle = m.top.findNode("homeSeriesFavTitle") m.homeSerFavFocus = [ m.top.findNode("homeSerFavFocus1"), m.top.findNode("homeSerFavFocus2"), m.top.findNode("homeSerFavFocus3"), m.top.findNode("homeSerFavFocus4") ] m.homeSerFavPoster = [ m.top.findNode("homeSerFavPoster1"), m.top.findNode("homeSerFavPoster2"), m.top.findNode("homeSerFavPoster3"), m.top.findNode("homeSerFavPoster4") ] m.homeSerFavStarBg = [ m.top.findNode("homeSerFavStarBg1"), m.top.findNode("homeSerFavStarBg2"), m.top.findNode("homeSerFavStarBg3"), m.top.findNode("homeSerFavStarBg4") ] m.homeSerFavStar = [ m.top.findNode("homeSerFavStar1"), m.top.findNode("homeSerFavStar2"), m.top.findNode("homeSerFavStar3"), m.top.findNode("homeSerFavStar4") ] m.homeSerFavLabel = [ m.top.findNode("homeSerFavLabel1"), m.top.findNode("homeSerFavLabel2"), m.top.findNode("homeSerFavLabel3"), m.top.findNode("homeSerFavLabel4") ] m.homeFavMoviesTitle = m.top.findNode("homeFavMoviesTitle") m.homeFavFocus = [ m.top.findNode("homeFavFocus1"), m.top.findNode("homeFavFocus2"), m.top.findNode("homeFavFocus3"), m.top.findNode("homeFavFocus4") ] m.homeFavPoster = [ m.top.findNode("homeFavPoster1"), m.top.findNode("homeFavPoster2"), m.top.findNode("homeFavPoster3"), m.top.findNode("homeFavPoster4") ] m.homeFavStarBg = [ m.top.findNode("homeFavStarBg1"), m.top.findNode("homeFavStarBg2"), m.top.findNode("homeFavStarBg3"), m.top.findNode("homeFavStarBg4") ] m.homeFavStar = [ m.top.findNode("homeFavStar1"), m.top.findNode("homeFavStar2"), m.top.findNode("homeFavStar3"), m.top.findNode("homeFavStar4") ] m.homeFavProgBg = [ m.top.findNode("homeFavProgBg1"), m.top.findNode("homeFavProgBg2"), m.top.findNode("homeFavProgBg3"), m.top.findNode("homeFavProgBg4") ] m.homeFavProgFill = [ m.top.findNode("homeFavProgFill1"), m.top.findNode("homeFavProgFill2"), m.top.findNode("homeFavProgFill3"), m.top.findNode("homeFavProgFill4") ] m.homeFavLabel = [ m.top.findNode("homeFavLabel1"), m.top.findNode("homeFavLabel2"), m.top.findNode("homeFavLabel3"), m.top.findNode("homeFavLabel4") ] m.homeContinueTitle = m.top.findNode("homeContinueTitle") m.homeContFocus = [ m.top.findNode("homeContFocus1"), m.top.findNode("homeContFocus2"), m.top.findNode("homeContFocus3"), m.top.findNode("homeContFocus4") ] m.homeContPoster = [ m.top.findNode("homeContPoster1"), m.top.findNode("homeContPoster2"), m.top.findNode("homeContPoster3"), m.top.findNode("homeContPoster4") ] m.homeContProgBg = [ m.top.findNode("homeContProgBg1"), m.top.findNode("homeContProgBg2"), m.top.findNode("homeContProgBg3"), m.top.findNode("homeContProgBg4") ] m.homeContProgFill = [ m.top.findNode("homeContProgFill1"), m.top.findNode("homeContProgFill2"), m.top.findNode("homeContProgFill3"), m.top.findNode("homeContProgFill4") ] m.homeContLabel = [ m.top.findNode("homeContLabel1"), m.top.findNode("homeContLabel2"), m.top.findNode("homeContLabel3"), m.top.findNode("homeContLabel4") ] m.epgBox = m.top.findNode("epgBox") m.epgNow = m.top.findNode("epgNow") m.epgNext = m.top.findNode("epgNext") m.movieGridTitle = m.top.findNode("movieGridTitle") m.posters = [ m.top.findNode("poster1"), m.top.findNode("poster2"), m.top.findNode("poster3"), m.top.findNode("poster4"), m.top.findNode("poster5"), m.top.findNode("poster6"), m.top.findNode("poster7"), m.top.findNode("poster8") ] m.posterFocus = [ m.top.findNode("posterFocus1"), m.top.findNode("posterFocus2"), m.top.findNode("posterFocus3"), m.top.findNode("posterFocus4"), m.top.findNode("posterFocus5"), m.top.findNode("posterFocus6"), m.top.findNode("posterFocus7"), m.top.findNode("posterFocus8") ] m.posterLabels = [ m.top.findNode("posterLabel1"), m.top.findNode("posterLabel2"), m.top.findNode("posterLabel3"), m.top.findNode("posterLabel4"), m.top.findNode("posterLabel5"), m.top.findNode("posterLabel6"), m.top.findNode("posterLabel7"), m.top.findNode("posterLabel8") ] m.posterFavBadgeBg = [ m.top.findNode("posterFavBadgeBg1"), m.top.findNode("posterFavBadgeBg2"), m.top.findNode("posterFavBadgeBg3"), m.top.findNode("posterFavBadgeBg4"), m.top.findNode("posterFavBadgeBg5"), m.top.findNode("posterFavBadgeBg6"), m.top.findNode("posterFavBadgeBg7"), m.top.findNode("posterFavBadgeBg8") ] m.posterFavBadge = [ m.top.findNode("posterFavBadge1"), m.top.findNode("posterFavBadge2"), m.top.findNode("posterFavBadge3"), m.top.findNode("posterFavBadge4"), m.top.findNode("posterFavBadge5"), m.top.findNode("posterFavBadge6"), m.top.findNode("posterFavBadge7"), m.top.findNode("posterFavBadge8") ] m.posterSeenBadgeBg = [ m.top.findNode("posterSeenBadgeBg1"), m.top.findNode("posterSeenBadgeBg2"), m.top.findNode("posterSeenBadgeBg3"), m.top.findNode("posterSeenBadgeBg4"), m.top.findNode("posterSeenBadgeBg5"), m.top.findNode("posterSeenBadgeBg6"), m.top.findNode("posterSeenBadgeBg7"), m.top.findNode("posterSeenBadgeBg8") ] m.posterSeenBadge = [ m.top.findNode("posterSeenBadge1"), m.top.findNode("posterSeenBadge2"), m.top.findNode("posterSeenBadge3"), m.top.findNode("posterSeenBadge4"), m.top.findNode("posterSeenBadge5"), m.top.findNode("posterSeenBadge6"), m.top.findNode("posterSeenBadge7"), m.top.findNode("posterSeenBadge8") ] m.posterProgressBg = [ m.top.findNode("posterProgressBg1"), m.top.findNode("posterProgressBg2"), m.top.findNode("posterProgressBg3"), m.top.findNode("posterProgressBg4"), m.top.findNode("posterProgressBg5"), m.top.findNode("posterProgressBg6"), m.top.findNode("posterProgressBg7"), m.top.findNode("posterProgressBg8") ] m.posterProgressFill = [ m.top.findNode("posterProgressFill1"), m.top.findNode("posterProgressFill2"), m.top.findNode("posterProgressFill3"), m.top.findNode("posterProgressFill4"), m.top.findNode("posterProgressFill5"), m.top.findNode("posterProgressFill6"), m.top.findNode("posterProgressFill7"), m.top.findNode("posterProgressFill8") ] m.detailPoster = m.top.findNode("detailPoster") m.detailTitle = m.top.findNode("detailTitle") m.detailMeta = m.top.findNode("detailMeta") m.detailPlot = m.top.findNode("detailPlot") m.detailButton = m.top.findNode("detailButton") m.movieSelectedTitle = m.top.findNode("movieSelectedTitle") m.moviePremiumBackdrop = m.top.findNode("moviePremiumBackdrop") m.moviePremiumDark = m.top.findNode("moviePremiumDark") m.moviePremiumAccent = m.top.findNode("moviePremiumAccent") m.moviePreviewPanel = m.top.findNode("moviePreviewPanel") m.moviePreviewPoster = m.top.findNode("moviePreviewPoster") m.moviePreviewTitle = m.top.findNode("moviePreviewTitle") m.moviePreviewMeta = m.top.findNode("moviePreviewMeta") m.moviePreviewPlot = [m.top.findNode("moviePreviewPlot1"), m.top.findNode("moviePreviewPlot2"), m.top.findNode("moviePreviewPlot3"), m.top.findNode("moviePreviewPlot4")] m.moviePreviewBadgeBg = m.top.findNode("moviePreviewBadgeBg") m.moviePreviewBadge = m.top.findNode("moviePreviewBadge") m.catalogPaneBg = m.top.findNode("catalogPaneBg") m.catalogHint = m.top.findNode("catalogHint") m.movieLiveSearchGroup = m.top.findNode("movieLiveSearchGroup") m.movieLiveSearchTitle = m.top.findNode("movieLiveSearchTitle") m.movieLiveSearchInput = m.top.findNode("movieLiveSearchInput") m.movieLiveSearchStatus = m.top.findNode("movieLiveSearchStatus") m.movieLiveSearchHint = m.top.findNode("movieLiveSearchHint") m.movieLiveSearchTypeChip = m.top.findNode("movieLiveSearchTypeChip") m.movieLiveSearchTypeLabel = m.top.findNode("movieLiveSearchTypeLabel") m.movieLiveResultBg = [m.top.findNode("movieLiveResultBg1"), m.top.findNode("movieLiveResultBg2"), m.top.findNode("movieLiveResultBg3"), m.top.findNode("movieLiveResultBg4"), m.top.findNode("movieLiveResultBg5")] m.movieLiveResultPoster = [m.top.findNode("movieLiveResultPoster1"), m.top.findNode("movieLiveResultPoster2"), m.top.findNode("movieLiveResultPoster3"), m.top.findNode("movieLiveResultPoster4"), m.top.findNode("movieLiveResultPoster5")] m.movieLiveResultLabel = [m.top.findNode("movieLiveResultLabel1"), m.top.findNode("movieLiveResultLabel2"), m.top.findNode("movieLiveResultLabel3"), m.top.findNode("movieLiveResultLabel4"), m.top.findNode("movieLiveResultLabel5")] m.seriesSideShade = m.top.findNode("seriesSideShade") m.seriesSideTitle = m.top.findNode("seriesSideTitle") m.seriesCatBg = [ m.top.findNode("seriesCatBg1"), m.top.findNode("seriesCatBg2"), m.top.findNode("seriesCatBg3"), m.top.findNode("seriesCatBg4"), m.top.findNode("seriesCatBg5"), m.top.findNode("seriesCatBg6"), m.top.findNode("seriesCatBg7"), m.top.findNode("seriesCatBg8"), m.top.findNode("seriesCatBg9"), m.top.findNode("seriesCatBg10") ] m.seriesCatFocus = [ m.top.findNode("seriesCatFocus1"), m.top.findNode("seriesCatFocus2"), m.top.findNode("seriesCatFocus3"), m.top.findNode("seriesCatFocus4"), m.top.findNode("seriesCatFocus5"), m.top.findNode("seriesCatFocus6"), m.top.findNode("seriesCatFocus7"), m.top.findNode("seriesCatFocus8"), m.top.findNode("seriesCatFocus9"), m.top.findNode("seriesCatFocus10") ] m.seriesCatLabels = [ m.top.findNode("seriesCatLabel1"), m.top.findNode("seriesCatLabel2"), m.top.findNode("seriesCatLabel3"), m.top.findNode("seriesCatLabel4"), m.top.findNode("seriesCatLabel5"), m.top.findNode("seriesCatLabel6"), m.top.findNode("seriesCatLabel7"), m.top.findNode("seriesCatLabel8"), m.top.findNode("seriesCatLabel9"), m.top.findNode("seriesCatLabel10") ] m.seriesCatCounts = [ m.top.findNode("seriesCatCount1"), m.top.findNode("seriesCatCount2"), m.top.findNode("seriesCatCount3"), m.top.findNode("seriesCatCount4"), m.top.findNode("seriesCatCount5"), m.top.findNode("seriesCatCount6"), m.top.findNode("seriesCatCount7"), m.top.findNode("seriesCatCount8"), m.top.findNode("seriesCatCount9"), m.top.findNode("seriesCatCount10") ] m.seriesCategories = [] m.seriesCategoryIndex = 0 m.seriesFocusArea = "categories" m.seriesPremiumBg = m.top.findNode("seriesPremiumBg") m.seriesPremiumDark = m.top.findNode("seriesPremiumDark") m.seriesPremiumLeftShade = m.top.findNode("seriesPremiumLeftShade") m.seriesPremiumLogo = m.top.findNode("seriesPremiumLogo") m.seriesPremiumTitle = m.top.findNode("seriesPremiumTitle") m.seriesPremiumMeta = m.top.findNode("seriesPremiumMeta") m.seriesPremiumPlot1 = m.top.findNode("seriesPremiumPlot1") m.seriesPremiumPlot2 = m.top.findNode("seriesPremiumPlot2") m.seriesPremiumPlot3 = m.top.findNode("seriesPremiumPlot3") m.seriesPremiumPlot4 = m.top.findNode("seriesPremiumPlot4") m.seriesPremiumPlot5 = m.top.findNode("seriesPremiumPlot5") m.seriesPremiumPlot6 = m.top.findNode("seriesPremiumPlot6") m.seriesPremiumBtnBg = [m.top.findNode("seriesPremiumBtnBg1"), m.top.findNode("seriesPremiumBtnBg2"), m.top.findNode("seriesPremiumBtnBg3")] m.seriesPremiumBtn = [m.top.findNode("seriesPremiumBtn1"), m.top.findNode("seriesPremiumBtn2"), m.top.findNode("seriesPremiumBtn3")] m.seriesPremiumContinue = m.top.findNode("seriesPremiumContinue") m.seriesPremiumSeasonTitle = m.top.findNode("seriesPremiumSeasonTitle") m.seriesPremiumSeasonBg = [m.top.findNode("seriesPremiumSeasonBg1"), m.top.findNode("seriesPremiumSeasonBg2"), m.top.findNode("seriesPremiumSeasonBg3"), m.top.findNode("seriesPremiumSeasonBg4")] m.seriesPremiumSeasonLabel = [m.top.findNode("seriesPremiumSeasonLabel1"), m.top.findNode("seriesPremiumSeasonLabel2"), m.top.findNode("seriesPremiumSeasonLabel3"), m.top.findNode("seriesPremiumSeasonLabel4")] m.seriesPremiumEpisodeTitle = m.top.findNode("seriesPremiumEpisodeTitle") m.seriesPremiumEpFocus = [m.top.findNode("seriesPremiumEpFocus1"), m.top.findNode("seriesPremiumEpFocus2"), m.top.findNode("seriesPremiumEpFocus3"), m.top.findNode("seriesPremiumEpFocus4")] m.seriesPremiumEpPoster = [m.top.findNode("seriesPremiumEpPoster1"), m.top.findNode("seriesPremiumEpPoster2"), m.top.findNode("seriesPremiumEpPoster3"), m.top.findNode("seriesPremiumEpPoster4")] m.seriesPremiumEpShade = [m.top.findNode("seriesPremiumEpShade1"), m.top.findNode("seriesPremiumEpShade2"), m.top.findNode("seriesPremiumEpShade3"), m.top.findNode("seriesPremiumEpShade4")] m.seriesPremiumEpLabel = [m.top.findNode("seriesPremiumEpLabel1"), m.top.findNode("seriesPremiumEpLabel2"), m.top.findNode("seriesPremiumEpLabel3"), m.top.findNode("seriesPremiumEpLabel4")] m.seriesPremiumEpProgBg = [m.top.findNode("seriesPremiumEpProgBg1"), m.top.findNode("seriesPremiumEpProgBg2"), m.top.findNode("seriesPremiumEpProgBg3"), m.top.findNode("seriesPremiumEpProgBg4")] m.seriesPremiumEpProgFill = [m.top.findNode("seriesPremiumEpProgFill1"), m.top.findNode("seriesPremiumEpProgFill2"), m.top.findNode("seriesPremiumEpProgFill3"), m.top.findNode("seriesPremiumEpProgFill4")] m.seriesPremiumFocusArea = "buttons" m.channelSideShade = m.top.findNode("channelSideShade") m.channelSideTitle = m.top.findNode("channelSideTitle") m.channelCatBg = [ m.top.findNode("channelCatBg1"), m.top.findNode("channelCatBg2"), m.top.findNode("channelCatBg3"), m.top.findNode("channelCatBg4"), m.top.findNode("channelCatBg5"), m.top.findNode("channelCatBg6"), m.top.findNode("channelCatBg7"), m.top.findNode("channelCatBg8"), m.top.findNode("channelCatBg9"), m.top.findNode("channelCatBg10") ] m.channelCatFocus = [ m.top.findNode("channelCatFocus1"), m.top.findNode("channelCatFocus2"), m.top.findNode("channelCatFocus3"), m.top.findNode("channelCatFocus4"), m.top.findNode("channelCatFocus5"), m.top.findNode("channelCatFocus6"), m.top.findNode("channelCatFocus7"), m.top.findNode("channelCatFocus8"), m.top.findNode("channelCatFocus9"), m.top.findNode("channelCatFocus10") ] m.channelCatLabels = [ m.top.findNode("channelCatLabel1"), m.top.findNode("channelCatLabel2"), m.top.findNode("channelCatLabel3"), m.top.findNode("channelCatLabel4"), m.top.findNode("channelCatLabel5"), m.top.findNode("channelCatLabel6"), m.top.findNode("channelCatLabel7"), m.top.findNode("channelCatLabel8"), m.top.findNode("channelCatLabel9"), m.top.findNode("channelCatLabel10") ] m.channelCatCounts = [ m.top.findNode("channelCatCount1"), m.top.findNode("channelCatCount2"), m.top.findNode("channelCatCount3"), m.top.findNode("channelCatCount4"), m.top.findNode("channelCatCount5"), m.top.findNode("channelCatCount6"), m.top.findNode("channelCatCount7"), m.top.findNode("channelCatCount8"), m.top.findNode("channelCatCount9"), m.top.findNode("channelCatCount10") ] m.channelCategories = [] m.channelCategoryIndex = 0 m.channelLoadedCategoryIndex = 0 m.channelFocusArea = "categories" m.channelListPaneBg = m.top.findNode("channelListPaneBg") m.channelListTitle = m.top.findNode("channelListTitle") m.channelItemBg = [ m.top.findNode("channelItemBg1"), m.top.findNode("channelItemBg2"), m.top.findNode("channelItemBg3"), m.top.findNode("channelItemBg4"), m.top.findNode("channelItemBg5"), m.top.findNode("channelItemBg6"), m.top.findNode("channelItemBg7"), m.top.findNode("channelItemBg8") ] m.channelItemFocus = [ m.top.findNode("channelItemFocus1"), m.top.findNode("channelItemFocus2"), m.top.findNode("channelItemFocus3"), m.top.findNode("channelItemFocus4"), m.top.findNode("channelItemFocus5"), m.top.findNode("channelItemFocus6"), m.top.findNode("channelItemFocus7"), m.top.findNode("channelItemFocus8") ] m.channelItemLogo = [ m.top.findNode("channelItemLogo1"), m.top.findNode("channelItemLogo2"), m.top.findNode("channelItemLogo3"), m.top.findNode("channelItemLogo4"), m.top.findNode("channelItemLogo5"), m.top.findNode("channelItemLogo6"), m.top.findNode("channelItemLogo7"), m.top.findNode("channelItemLogo8") ] m.channelItemLabel = [ m.top.findNode("channelItemLabel1"), m.top.findNode("channelItemLabel2"), m.top.findNode("channelItemLabel3"), m.top.findNode("channelItemLabel4"), m.top.findNode("channelItemLabel5"), m.top.findNode("channelItemLabel6"), m.top.findNode("channelItemLabel7"), m.top.findNode("channelItemLabel8") ] m.channelPreviewBox = m.top.findNode("channelPreviewBox") m.channelPreviewTag = m.top.findNode("channelPreviewTag") m.channelPreviewLogo = m.top.findNode("channelPreviewLogo") m.channelPreviewTitle = m.top.findNode("channelPreviewTitle") m.channelPreviewMeta = m.top.findNode("channelPreviewMeta") m.channelPreviewInfoBg = m.top.findNode("channelPreviewInfoBg") m.channelPreviewNow = m.top.findNode("channelPreviewNow") m.channelPreviewNext = m.top.findNode("channelPreviewNext") m.channelPreviewChip1 = m.top.findNode("channelPreviewChip1") m.channelPreviewChip2 = m.top.findNode("channelPreviewChip2") m.channelPreviewChip3 = m.top.findNode("channelPreviewChip3") m.channelPreviewHint1 = m.top.findNode("channelPreviewHint1") m.channelPreviewHint2 = m.top.findNode("channelPreviewHint2") m.channelPreviewHint3 = m.top.findNode("channelPreviewHint3") m.movieSideShade = m.top.findNode("movieSideShade") m.movieSideTitle = m.top.findNode("movieSideTitle") m.movieCatBg = [ m.top.findNode("movieCatBg1"), m.top.findNode("movieCatBg2"), m.top.findNode("movieCatBg3"), m.top.findNode("movieCatBg4"), m.top.findNode("movieCatBg5"), m.top.findNode("movieCatBg6"), m.top.findNode("movieCatBg7"), m.top.findNode("movieCatBg8"), m.top.findNode("movieCatBg9"), m.top.findNode("movieCatBg10") ] m.movieCatFocus = [ m.top.findNode("movieCatFocus1"), m.top.findNode("movieCatFocus2"), m.top.findNode("movieCatFocus3"), m.top.findNode("movieCatFocus4"), m.top.findNode("movieCatFocus5"), m.top.findNode("movieCatFocus6"), m.top.findNode("movieCatFocus7"), m.top.findNode("movieCatFocus8"), m.top.findNode("movieCatFocus9"), m.top.findNode("movieCatFocus10") ] m.movieCatLabels = [ m.top.findNode("movieCatLabel1"), m.top.findNode("movieCatLabel2"), m.top.findNode("movieCatLabel3"), m.top.findNode("movieCatLabel4"), m.top.findNode("movieCatLabel5"), m.top.findNode("movieCatLabel6"), m.top.findNode("movieCatLabel7"), m.top.findNode("movieCatLabel8"), m.top.findNode("movieCatLabel9"), m.top.findNode("movieCatLabel10") ] m.movieCatCounts = [ m.top.findNode("movieCatCount1"), m.top.findNode("movieCatCount2"), m.top.findNode("movieCatCount3"), m.top.findNode("movieCatCount4"), m.top.findNode("movieCatCount5"), m.top.findNode("movieCatCount6"), m.top.findNode("movieCatCount7"), m.top.findNode("movieCatCount8"), m.top.findNode("movieCatCount9"), m.top.findNode("movieCatCount10") ] m.movieCategories = [] m.movieCategoryIndex = 0 m.movieFocusArea = "categories" m.detailPlot1 = m.top.findNode("detailPlot1") m.detailPlot2 = m.top.findNode("detailPlot2") m.detailPlot3 = m.top.findNode("detailPlot3") m.detailPlot4 = m.top.findNode("detailPlot4") m.detailPlot5 = m.top.findNode("detailPlot5") m.detailPlot6 = m.top.findNode("detailPlot6") m.video = m.top.findNode("video") m.playerOverlay = m.top.findNode("playerOverlay") m.playerTitle = m.top.findNode("playerTitle") m.playerHint = m.top.findNode("playerHint") m.playerEpg = m.top.findNode("playerEpg") m.movieSimplePanel = m.top.findNode("movieSimplePanel") m.movieSimpleTitle = m.top.findNode("movieSimpleTitle") m.movieSimpleBarBg = m.top.findNode("movieSimpleBarBg") m.movieSimpleBarFill = m.top.findNode("movieSimpleBarFill") m.movieSimpleTime = m.top.findNode("movieSimpleTime") m.movieSimpleTotal = m.top.findNode("movieSimpleTotal") m.movieSimpleHint = m.top.findNode("movieSimpleHint") m.detailBackdrop = m.top.findNode("detailBackdrop") m.detailDarkLayer = m.top.findNode("detailDarkLayer") m.detailInfoPanel = m.top.findNode("detailInfoPanel") m.detailButtonBg = m.top.findNode("detailButtonBg") m.detailFavButtonBg = m.top.findNode("detailFavButtonBg") m.detailFavButton = m.top.findNode("detailFavButton") m.timer = m.top.findNode("startTimer") m.registerTask = m.top.findNode("registerTask") m.checkTask = m.top.findNode("checkTask") m.apiGetTask = m.top.findNode("apiGetTask") m.apiMovieTask = m.top.findNode("apiMovieTask") m.apiLiveSearchTask = m.top.findNode("apiLiveSearchTask") m.movieSearchDebounceTimer = m.top.findNode("movieSearchDebounceTimer") m.apiPostTask = m.top.findNode("apiPostTask") m.epgTask = m.top.findNode("epgTask") m.sportsApiTask = m.top.findNode("sportsApiTask") m.sportsChannelResolverTask = m.top.findNode("sportsChannelResolverTask") m.overlayTimer = m.top.findNode("overlayTimer") if m.overlayTimer <> invalid then m.overlayTimer.observeField("fire", "onOverlayTimer") m.liveOpenTimer = m.top.findNode("liveOpenTimer") m.liveReconnectTimer = m.top.findNode("liveReconnectTimer") m.liveRestartDelayTimer = m.top.findNode("liveRestartDelayTimer") m.liveStableTimer = m.top.findNode("liveStableTimer") m.progressSaveTimer = m.top.findNode("progressSaveTimer") m.playerProgressTimer = m.top.findNode("playerProgressTimer") ' Estado visual separado do Video. Alguns modelos mantêm position/duration ' do episódio anterior por alguns instantes ao trocar de conteúdo. m.playerUiReady = false m.playerUiPosition = 0 m.playerUiDuration = 0 m.playerProgressHasValidSample = false m.playerPendingSeekSeconds = 0 m.playerPendingSeekApplied = true m.liveOpenUsingFastUrl = false m.liveFallbackRequested = false m.liveFallbackStreamId = "" m.liveFallbackTitle = "" m.liveResolvedUrls = {} m.liveReconnectAttempts = 0 m.liveMaxReconnectAttempts = 3 m.liveRestartPending = false m.liveRecoveringVisible = false m.currentLiveUrl = "" m.currentLiveFmt = "hls" m.currentLiveTitle = "" m.currentPlayUrl = "" m.currentPlayTitle = "" m.currentPlayFmt = "" m.currentPlayDetectedFormat = "" m.currentPlayMkvRetried = false m.epgTimeoutTimer = m.top.findNode("epgTimeoutTimer") m.epgHideTimer = m.top.findNode("epgHideTimer") m.movieSimpleHideTimer = m.top.findNode("movieSimpleHideTimer") m.holdSeekTimer = m.top.findNode("holdSeekTimer") m.holdSeekDirection = 0 m.holdSeekStep = 0 m.holdSeekTicks = 0 ' Tempo de avanço/retrocesso configurável pelo painel. ' Padrão seguro: 30s. Em séries limitamos a no máximo 60s por toque, ' evitando um salto grande que possa encerrar o episódio sem querer. m.playerSeekMovieSeconds = 30 m.playerSeekSeriesSeconds = 30 m.nextEpisodeTimer = m.top.findNode("nextEpisodeTimer") m.nextEpisodePanel = m.top.findNode("nextEpisodePanel") m.nextEpisodeSmall = m.top.findNode("nextEpisodeSmall") m.nextEpisodeTitle = m.top.findNode("nextEpisodeTitle") m.nextEpisodeHint = m.top.findNode("nextEpisodeHint") m.nextEpisodeButton = m.top.findNode("nextEpisodeButton") m.nextEpisodeButtonText = m.top.findNode("nextEpisodeButtonText") m.nextEpisodeEnabled = true m.nextEpisodePromptSeconds = 60 m.nextEpisodeButtonLabel = "PRÓXIMO EPISÓDIO" m.nextEpisodeButtonColor = "0xDC2626FF" m.nextEpisodePromptVisible = false m.nextEpisodeOffered = false m.nextEpisodeItem = invalid ' Prompt premium: continuar de onde parou ou iniciar do começo. m.resumePromptGroup = m.top.findNode("resumePromptGroup") m.resumePromptTitle = m.top.findNode("resumePromptTitle") m.resumePromptMessage = m.top.findNode("resumePromptMessage") m.resumePromptTime = m.top.findNode("resumePromptTime") m.resumePromptBtnBg1 = m.top.findNode("resumePromptBtnBg1") m.resumePromptBtnBg2 = m.top.findNode("resumePromptBtnBg2") m.resumePromptBtn1 = m.top.findNode("resumePromptBtn1") m.resumePromptBtn2 = m.top.findNode("resumePromptBtn2") m.resumePromptHint = m.top.findNode("resumePromptHint") m.resumePromptVisible = false m.resumePromptChoice = 0 m.resumePromptPrevMode = "" m.resumePendingKind = "" m.resumePendingTitle = "" m.resumePendingUrl = "" m.resumePendingFmt = "" m.resumePendingReturnMode = "" m.resumePendingItem = invalid m.resumeForceStart = false m.deviceId = CreateObject("roDeviceInfo").GetChannelClientId() m.shortDeviceCode = awGetShortDeviceCode(m.deviceId) m.menuIndex = 0 m.page = 0 m.mode = "activation" m.items = [] m.homeItems = ["CANAIS AO VIVO", "FAVORITOS", "FILMES", "SÉRIES", "LOGIN / TROCAR USUÁRIO", "SAIR / LIMPAR LOGIN"] m.searchQuery = "" m.searchPage = 0 m.searchIndex = 0 m.allMovieItems = [] m.liveSearchItems = [] m.liveSearchRequestQuery = "" m.liveSearchLoadedQuery = "" m.liveSearchLoading = false m.timer.observeField("fire", "onStartTimer") m.registerTask.observeField("result", "onRegisterResult") m.checkTask.observeField("result", "onCheckResult") m.apiGetTask.observeField("result", "onApiGetResult") m.apiMovieTask.observeField("result", "onApiMovieResult") if m.apiLiveSearchTask <> invalid then m.apiLiveSearchTask.observeField("result", "onApiLiveSearchResult") if m.movieSearchDebounceTimer <> invalid then m.movieSearchDebounceTimer.observeField("fire", "onMovieSearchDebounce") if m.movieLoadingTimer <> invalid then m.movieLoadingTimer.observeField("fire", "onMovieLoadingTimer") if m.loginFocusTimer <> invalid then m.loginFocusTimer.observeField("fire", "onLoginFocusTimer") if m.startupAnimTimer <> invalid then m.startupAnimTimer.observeField("fire", "onStartupAnimTimer") if m.startupProceedTimer <> invalid then m.startupProceedTimer.observeField("fire", "onStartupProceedTimer") m.apiPostTask.observeField("result", "onApiPostResult") m.epgTask.observeField("result", "onEpgResult") if m.sportsApiTask <> invalid then m.sportsApiTask.observeField("result", "onSportsApiResult") if m.sportsChannelResolverTask <> invalid then m.sportsChannelResolverTask.observeField("result", "onSportsChannelResolverResult") m.video.observeField("state", "onVideoState") m.overlayTimer.observeField("fire", "hidePlayerOverlay") if m.liveOpenTimer <> invalid then m.liveOpenTimer.observeField("fire", "onLiveOpenTimeout") if m.liveReconnectTimer <> invalid then m.liveReconnectTimer.observeField("fire", "onLiveReconnectTimer") if m.liveRestartDelayTimer <> invalid then m.liveRestartDelayTimer.observeField("fire", "onLiveRestartDelayTimer") if m.liveStableTimer <> invalid then m.liveStableTimer.observeField("fire", "onLiveStableTimer") if m.progressSaveTimer <> invalid then m.progressSaveTimer.observeField("fire", "onProgressSaveTimer") if m.playerProgressTimer <> invalid then m.playerProgressTimer.observeField("fire", "onPlayerProgressTimer") m.epgTimeoutTimer.observeField("fire", "onEpgTimeout") m.epgHideTimer.observeField("fire", "hidePlayerOverlay") m.movieSimpleHideTimer.observeField("fire", "hideMovieSimpleBar") if m.holdSeekTimer <> invalid then m.holdSeekTimer.observeField("fire", "onHoldSeekTimer") if m.nextEpisodeTimer <> invalid then m.nextEpisodeTimer.observeField("fire", "onNextEpisodeTimer") m.top.setFocus(true) applyAppBackground() loadSavedAssetConfig() applyDynamicTextsToScreen() showStartupAnimation() token = readToken() m.pendingStartupToken = token if m.startupProceedTimer <> invalid then m.startupProceedTimer.control = "start" else proceedAfterStartupAnimation() end if end sub sub showStartupAnimation() hideActivationText() if m.footer <> invalid then m.footer.text = "" m.footer.visible = false end if if m.startupAnimGroup <> invalid then m.startupAnimGroup.visible = true if m.startupAnimNinja <> invalid then m.startupNinjaFrameIndex = 0 if m.startupNinjaFrames <> invalid and m.startupNinjaFrames.Count() > 0 then m.startupAnimNinja.uri = m.startupNinjaFrames[0] end if end if if m.startupAnimTimer <> invalid then m.startupAnimTimer.control = "start" end sub sub hideStartupAnimation() if m.startupAnimTimer <> invalid then m.startupAnimTimer.control = "stop" if m.startupProceedTimer <> invalid then m.startupProceedTimer.control = "stop" if m.startupAnimGroup <> invalid then m.startupAnimGroup.visible = false end sub sub onStartupAnimTimer() if m.startupAnimNinja = invalid then return if m.startupNinjaFrames = invalid then return total = m.startupNinjaFrames.Count() if total <= 0 then return m.startupNinjaFrameIndex = m.startupNinjaFrameIndex + 1 if m.startupNinjaFrameIndex >= total then m.startupNinjaFrameIndex = 0 m.startupAnimNinja.uri = m.startupNinjaFrames[m.startupNinjaFrameIndex] end sub sub onStartupProceedTimer() proceedAfterStartupAnimation() end sub sub proceedAfterStartupAnimation() hideStartupAnimation() token = m.pendingStartupToken if token <> invalid and token <> "" then m.token = token m.mode = "home_start" loadAppBanners() else showLoginScreen() loadAppBanners() end if end sub sub showActivationText() if m.code <> invalid then m.code.visible = true end if if m.status <> invalid then m.status.visible = true end if if m.hint <> invalid then m.hint.visible = true end if end sub sub hideActivationText() if m.code <> invalid then m.code.text = "" m.code.visible = false end if if m.status <> invalid then m.status.text = "" m.status.visible = false end if if m.hint <> invalid then m.hint.text = "" m.hint.visible = false end if end sub sub onStartTimer() registerDevice() end sub sub registerDevice() hideStartupAnimation() clearHome() showActivationText() m.mode = "activation" m.status.text = m.startupLoadingText m.registerTask.apiBase = m.apiBase m.registerTask.deviceId = m.deviceId m.registerTask.deviceModel = CreateObject("roDeviceInfo").GetModel() m.registerTask.control = "RUN" end sub sub onRegisterResult() res = m.registerTask.result if res <> invalid and res.success = true then m.activationCode = res.activation_code m.code.text = "Código: " + m.activationCode m.status.text = "Aguardando ativação no painel" m.hint.text = m.startupHintText m.footer.text = "" checkActivation() else m.code.text = "Erro na API" if res <> invalid and res.message <> invalid then m.status.text = res.message else m.status.text = "Verifique a URL/IP do painel" end if end if end sub sub checkActivation() if m.activationCode = invalid then return if m.mode = "activation" then showActivationText() end if m.status.text = "Consultando ativação..." m.checkTask.apiBase = m.apiBase m.checkTask.deviceId = m.deviceId m.checkTask.activationCode = m.activationCode m.checkTask.control = "RUN" end sub sub onCheckResult() res = m.checkTask.result if res <> invalid and res.status = "active" then sec = CreateObject("roRegistrySection", "awplayer") sec.Write("token", res.token) sec.Flush() m.token = res.token saveLoginSessionSafe(res) showHome() else if res <> invalid and res.status = "pending" then m.status.text = "Aguardando ativação... pressione OK para consultar" else m.status.text = "Sem resposta da API. Confira domínio/IP do painel." end if end sub sub setPremiumHomeVisible(show as Boolean) nodes = [ m.awRedHomeBg, m.heroCard, m.heroTitle, m.heroSub, m.rowTitle, m.card1, m.card2, m.card3, m.card4, m.card5, m.card6, m.cardLabel1, m.cardLabel2, m.cardLabel3, m.cardLabel4, m.cardLabel5, m.cardLabel6, m.cardIcon1, m.cardIcon2, m.cardIcon3, m.cardIcon4, m.cardIcon5, m.cardIcon6, m.homeLogo, m.expiryBg, m.expiryLabel, m.premiumHint, m.reloadBtnBg ] for each n in nodes if n <> invalid then n.visible = show end for end sub sub renderPremiumHome() applyHomeBackground() setPremiumHomeVisible(true) if m.title <> invalid then m.title.visible = false if m.title <> invalid then m.title.text = "" clearMainMenuTexts() hideHomeBanner() hideAllHomeRows() hideListEpg() if m.homeLogo <> invalid then m.homeLogo.width = 210 m.homeLogo.height = 145 m.homeLogo.translation = [535, 50] end if if m.heroCard <> invalid then m.heroCard.visible = false if m.heroTitle <> invalid then m.heroTitle.visible = false if m.heroSub <> invalid then m.heroSub.visible = false if m.rowTitle <> invalid then m.rowTitle.visible = false m.cardLabel1.text = m.homeLiveText m.cardLabel2.text = m.homeMoviesText m.cardLabel3.text = m.homeSeriesText m.cardLabel4.text = m.homeSportsText m.cardLabel5.text = m.homeListText m.cardLabel6.text = m.homeSettingsText if m.premiumHint <> invalid then m.premiumHint.text = m.homeReloadText m.premiumHint.visible = true end if renderPremiumSelection() renderHomeExpiration() end sub sub renderPremiumSelection() cards = [m.card1, m.card2, m.card3, m.card4, m.card5, m.card6] for i = 0 to 5 if i < cards.Count() and cards[i] <> invalid then if m.menuIndex = i then cards[i].color = "0x7F0B0BFF" else cards[i].color = "0x26080BCC" end if end if end for if m.premiumHint <> invalid then m.premiumHint.visible = true if m.menuIndex = 6 then m.premiumHint.color = "0xFFFFFFFF" else m.premiumHint.color = "0xE5E7EBFF" end if end if if m.reloadBtnBg <> invalid then m.reloadBtnBg.visible = true if m.menuIndex = 6 then m.reloadBtnBg.color = "0x7F0B0BFF" else m.reloadBtnBg.color = "0x22080BCC" end if end if hideAllHomeRows() hideListEpg() end sub function awIsNumericText(v as String) as Boolean if v = invalid then return false s = v.Trim() if s = "" then return false for i = 1 to Len(s) ch = Mid(s, i, 1) if ch < "0" or ch > "9" then return false end for return true end function function awPad2(n as Integer) as String s = n.ToStr() if Len(s) < 2 then return "0" + s return s end function function awFormatExpiryDate(raw as Dynamic) as String if raw = invalid then return "" s = raw.ToStr().Trim() if s = "" then return "" low = LCase(s) if low.InStr("vital") >= 0 or low.InStr("lifetime") >= 0 or low.InStr("unlimited") >= 0 then return "Vitalício" end if ' Unix timestamp: 1716230400 ou 1716230400000 if awIsNumericText(s) then if Len(s) >= 13 then s = Left(s, 10) ts = s.ToInt() if ts > 1000000000 then dt = CreateObject("roDateTime") dt.FromSeconds(ts) return awPad2(dt.GetDayOfMonth()) + "/" + awPad2(dt.GetMonth()) + "/" + dt.GetYear().ToStr() end if end if ' Formato ISO: 2026-06-20 ou 2026-06-20 23:59:59 if Len(s) >= 10 and Mid(s,5,1) = "-" and Mid(s,8,1) = "-" then return Mid(s,9,2) + "/" + Mid(s,6,2) + "/" + Mid(s,1,4) end if ' Formato BR já pronto: 20/06/2026 if Len(s) >= 10 and Mid(s,3,1) = "/" and Mid(s,6,1) = "/" then return Left(s,10) end if return s end function function awFindExpiryRaw(obj as Dynamic) as String if obj = invalid then return "" if Type(obj) <> "roAssociativeArray" then return "" keys = ["exp_date", "expires_at", "expiration", "expiration_date", "expire_date", "expires", "valid_until", "vencimento", "data_vencimento", "end_date", "due_date"] for each k in keys if obj.DoesExist(k) then v = obj[k] if v <> invalid and v.ToStr() <> "" then return v.ToStr() end if end for nested = ["user_info", "user", "account", "client", "data", "profile"] for each nk in nested if obj.DoesExist(nk) then found = awFindExpiryRaw(obj[nk]) if found <> "" then return found end if end for return "" end function function awHomeExpirationText() as String sec = CreateObject("roRegistrySection", "awplayer") raw = "" if sec.Exists("expiry_raw") then raw = sec.Read("expiry_raw") if raw = invalid or raw = "" then return "" formatted = awFormatExpiryDate(raw) if formatted = invalid or formatted = "" then return "" prefix = "Vencimento da lista:" if m.homeExpirationPrefix <> invalid and m.homeExpirationPrefix <> "" then prefix = m.homeExpirationPrefix return prefix + " " + formatted end function sub renderHomeExpiration() if m.expiryLabel = invalid then return txt = awHomeExpirationText() if txt <> "" then m.expiryLabel.text = txt m.expiryLabel.visible = true if m.expiryBg <> invalid then m.expiryBg.visible = true else m.expiryLabel.visible = false if m.expiryBg <> invalid then m.expiryBg.visible = false end if end sub sub loadHomeInfoForExpiration() if m.apiGetTask = invalid then return if m.token = invalid or m.token = "" then return m.homeInfoPending = true m.apiGetTask.control = "STOP" m.apiGetTask.url = m.apiBase + "/api/roku/home.php?token=" + m.token + "&t=" + awNowSecondsText() m.apiGetTask.control = "RUN" end sub function getHomeContinueList() as Object sec = CreateObject("roRegistrySection", "awplayer") rawList = "" if sec.Exists("home_continue_movies") then rawList = sec.Read("home_continue_movies") end if if rawList = invalid or rawList = "" then return [] end if parsedList = ParseJson(rawList) if parsedList = invalid then return [] end if if type(parsedList) <> "roArray" then return [] end if return parsedList end function sub saveHomeContinueList(listData as Object) sec = CreateObject("roRegistrySection", "awplayer") sec.Write("home_continue_movies", FormatJson(listData)) sec.Flush() end sub function makeHomeContinueItem(item as Dynamic) as Object movieItem = {} if item = invalid then return movieItem end if movieItem.stream_id = movieIdOf(item) movieItem.name = itemTitle(item) movieItem.stream_display_name = itemTitle(item) movieItem.cover = movieCover(item) movieItem.cover_big = movieCover(item) movieItem.plot = moviePlot(item) movieItem.genre = "" movieItem.rating = "" movieItem.duration = "" movieItem.releasedate = "" if item.genre <> invalid then movieItem.genre = item.genre.ToStr() end if if item.rating <> invalid then movieItem.rating = item.rating.ToStr() end if if item.duration <> invalid then movieItem.duration = item.duration.ToStr() end if if item.releasedate <> invalid then movieItem.releasedate = item.releasedate.ToStr() end if if item.container_extension <> invalid then movieItem.container_extension = item.container_extension.ToStr() end if return movieItem end function sub addMovieToHomeContinue() if m.currentMovie = invalid then return end if currentId = movieIdOf(m.currentMovie) if currentId = "" then return end if oldList = getHomeContinueList() newList = [] newList.Push(makeHomeContinueItem(m.currentMovie)) addedCount = 1 for each oldItem in oldList oldId = movieIdOf(oldItem) if oldId <> "" and oldId <> currentId then if addedCount < 10 then newList.Push(oldItem) addedCount = addedCount + 1 end if end if end for saveHomeContinueList(newList) end sub sub hideHomeContinueRow() if m.homeContinueTitle <> invalid then m.homeContinueTitle.visible = false end if for i = 0 to 3 if m.homeContFocus[i] <> invalid then m.homeContFocus[i].visible = false end if if m.homeContPoster[i] <> invalid then m.homeContPoster[i].visible = false end if if m.homeContProgBg[i] <> invalid then m.homeContProgBg[i].visible = false end if if m.homeContProgFill[i] <> invalid then m.homeContProgFill[i].visible = false end if if m.homeContLabel[i] <> invalid then m.homeContLabel[i].visible = false m.homeContLabel[i].text = "" end if end for end sub sub renderHomeContinueRow() m.homeContinueItems = getHomeContinueList() if m.homeContinueItems.Count() = 0 then hideHomeContinueRow() return end if if m.homeContinueTitle <> invalid then m.homeContinueTitle.visible = true end if if m.homeContinueIndex = invalid then m.homeContinueIndex = 0 end if maxItems = m.homeContinueItems.Count() if maxItems > 4 then maxItems = 4 end if for i = 0 to 3 if i < maxItems then homeItem = m.homeContinueItems[i] if m.homeContPoster[i] <> invalid then m.homeContPoster[i].uri = movieCover(homeItem) m.homeContPoster[i].visible = true end if progressPct = movieProgressPercent(homeItem) if m.homeContProgBg[i] <> invalid then m.homeContProgBg[i].visible = true end if if m.homeContProgFill[i] <> invalid then fillHome = Int((96 * progressPct) / 100) if fillHome < 6 then fillHome = 6 end if m.homeContProgFill[i].width = fillHome m.homeContProgFill[i].visible = true end if if m.homeContLabel[i] <> invalid then m.homeContLabel[i].text = awCutText(itemTitle(homeItem), 14) m.homeContLabel[i].visible = true end if if m.homeContFocus[i] <> invalid then if m.homeArea = "continue" and i = m.homeContinueIndex then m.homeContFocus[i].visible = true else m.homeContFocus[i].visible = false end if end if else if m.homeContFocus[i] <> invalid then m.homeContFocus[i].visible = false end if if m.homeContPoster[i] <> invalid then m.homeContPoster[i].visible = false end if if m.homeContProgBg[i] <> invalid then m.homeContProgBg[i].visible = false end if if m.homeContProgFill[i] <> invalid then m.homeContProgFill[i].visible = false end if if m.homeContLabel[i] <> invalid then m.homeContLabel[i].visible = false end if end if end for end sub sub openHomeContinueItem() if m.homeContinueItems = invalid then return end if if m.homeContinueItems.Count() = 0 then return end if if m.homeContinueIndex < 0 then m.homeContinueIndex = 0 end if if m.homeContinueIndex >= m.homeContinueItems.Count() then m.homeContinueIndex = 0 end if m.currentMovie = m.homeContinueItems[m.homeContinueIndex] m.returnModeAfterPlayer = "movie_detail" showMovieDetail() end sub sub hideHomeFavMoviesRow() if m.homeFavMoviesTitle <> invalid then m.homeFavMoviesTitle.visible = false end if for i = 0 to 3 if m.homeFavFocus[i] <> invalid then m.homeFavFocus[i].visible = false if m.homeFavPoster[i] <> invalid then m.homeFavPoster[i].visible = false if m.homeFavStarBg[i] <> invalid then m.homeFavStarBg[i].visible = false if m.homeFavStar[i] <> invalid then m.homeFavStar[i].visible = false if m.homeFavProgBg[i] <> invalid then m.homeFavProgBg[i].visible = false if m.homeFavProgFill[i] <> invalid then m.homeFavProgFill[i].visible = false if m.homeFavLabel[i] <> invalid then m.homeFavLabel[i].visible = false m.homeFavLabel[i].text = "" end if end for end sub sub updateHomeFavLayout() useFullWidth = true if m.homeContinueItems <> invalid and m.homeContinueItems.Count() > 0 then useFullWidth = false end if if useFullWidth then if m.homeFavMoviesTitle <> invalid then m.homeFavMoviesTitle.translation = [90,500] m.homeFavMoviesTitle.width = 1100 end if focusX = [88, 258, 428, 598] posterX = [92, 262, 432, 602] starBgX = [98, 268, 438, 608] progX = [92, 262, 432, 602] labelX = [82, 252, 422, 592] else if m.homeFavMoviesTitle <> invalid then m.homeFavMoviesTitle.translation = [650,500] m.homeFavMoviesTitle.width = 520 end if focusX = [648, 778, 908, 1038] posterX = [652, 782, 912, 1042] starBgX = [658, 788, 918, 1048] progX = [652, 782, 912, 1042] labelX = [642, 772, 902, 1032] end if for i = 0 to 3 if m.homeFavFocus[i] <> invalid then m.homeFavFocus[i].translation = [focusX[i], 532] if m.homeFavPoster[i] <> invalid then m.homeFavPoster[i].translation = [posterX[i], 537] if m.homeFavStarBg[i] <> invalid then m.homeFavStarBg[i].translation = [starBgX[i], 542] if m.homeFavStar[i] <> invalid then m.homeFavStar[i].translation = [starBgX[i], 543] if m.homeFavProgBg[i] <> invalid then m.homeFavProgBg[i].translation = [progX[i], 675] if m.homeFavProgFill[i] <> invalid then m.homeFavProgFill[i].translation = [progX[i], 675] if m.homeFavLabel[i] <> invalid then m.homeFavLabel[i].translation = [labelX[i], 687] end for end sub function getHomeFavWindowStart(total As Integer, visibleCount As Integer) As Integer startIndex = 0 if m.homeFavWindowStart <> invalid then startIndex = m.homeFavWindowStart if m.homeFavIndex < startIndex then startIndex = m.homeFavIndex else if m.homeFavIndex >= startIndex + visibleCount then startIndex = m.homeFavIndex - visibleCount + 1 end if maxStart = total - visibleCount if maxStart < 0 then maxStart = 0 if startIndex > maxStart then startIndex = maxStart if startIndex < 0 then startIndex = 0 m.homeFavWindowStart = startIndex return startIndex end function sub renderHomeFavMoviesRow() m.homeFavMovieItems = getMovieFavorites() if m.homeFavMovieItems.Count() = 0 then hideHomeFavMoviesRow() hideHomeSeriesRows() return end if updateHomeFavLayout() if m.homeFavMoviesTitle <> invalid then totalFavText = m.homeFavMovieItems.Count().ToStr().Trim() if m.homeFavMovieItems.Count() > 4 then m.homeFavMoviesTitle.text = "MEUS FILMES FAVORITOS (" + totalFavText + ")" else m.homeFavMoviesTitle.text = "MEUS FILMES FAVORITOS" end if m.homeFavMoviesTitle.visible = true end if if m.homeFavIndex = invalid then m.homeFavIndex = 0 if m.homeFavIndex < 0 then m.homeFavIndex = 0 if m.homeFavIndex >= m.homeFavMovieItems.Count() then m.homeFavIndex = m.homeFavMovieItems.Count() - 1 visibleCount = 4 startIndex = getHomeFavWindowStart(m.homeFavMovieItems.Count(), visibleCount) for i = 0 to 3 itemIndex = startIndex + i if itemIndex < m.homeFavMovieItems.Count() then favItem = m.homeFavMovieItems[itemIndex] if m.homeFavPoster[i] <> invalid then m.homeFavPoster[i].uri = movieCover(favItem) m.homeFavPoster[i].visible = true end if if m.homeFavStarBg[i] <> invalid then m.homeFavStarBg[i].visible = true if m.homeFavStar[i] <> invalid then m.homeFavStar[i].visible = true favPct = movieProgressPercent(favItem) if favPct > 0 then if m.homeFavProgBg[i] <> invalid then m.homeFavProgBg[i].visible = true if m.homeFavProgFill[i] <> invalid then favFill = Int((96 * favPct) / 100) if favFill < 6 then favFill = 6 m.homeFavProgFill[i].width = favFill m.homeFavProgFill[i].visible = true end if else if m.homeFavProgBg[i] <> invalid then m.homeFavProgBg[i].visible = false if m.homeFavProgFill[i] <> invalid then m.homeFavProgFill[i].visible = false end if if m.homeFavLabel[i] <> invalid then m.homeFavLabel[i].text = awCutText(itemTitle(favItem), 14) m.homeFavLabel[i].visible = true end if if m.homeFavFocus[i] <> invalid then if m.homeArea = "favmovies" and itemIndex = m.homeFavIndex then m.homeFavFocus[i].visible = true else m.homeFavFocus[i].visible = false end if end if else if m.homeFavFocus[i] <> invalid then m.homeFavFocus[i].visible = false if m.homeFavPoster[i] <> invalid then m.homeFavPoster[i].visible = false if m.homeFavStarBg[i] <> invalid then m.homeFavStarBg[i].visible = false if m.homeFavStar[i] <> invalid then m.homeFavStar[i].visible = false if m.homeFavProgBg[i] <> invalid then m.homeFavProgBg[i].visible = false if m.homeFavProgFill[i] <> invalid then m.homeFavProgFill[i].visible = false if m.homeFavLabel[i] <> invalid then m.homeFavLabel[i].visible = false end if end for end sub sub openHomeFavMovieItem() if m.homeFavMovieItems = invalid then return if m.homeFavMovieItems.Count() = 0 then return if m.homeFavIndex < 0 then m.homeFavIndex = 0 if m.homeFavIndex >= m.homeFavMovieItems.Count() then m.homeFavIndex = 0 m.currentMovie = m.homeFavMovieItems[m.homeFavIndex] m.returnModeAfterPlayer = "movie_detail" showMovieDetail() end sub function awDnsToArray(raw as Dynamic) as Object out = [] if raw = invalid then return out end if t = Type(raw) if t = "roArray" then return raw end if if t = "roAssociativeArray" then if raw.dns <> invalid then return awDnsToArray(raw.dns) end if ' Caso venha como objeto com chaves "0", "1", "2" i = 0 while true key = i.ToStr() if raw.DoesExist(key) then out.Push(raw[key]) i = i + 1 else exit while end if end while if out.Count() > 0 then return out end if ' Caso venha um único DNS direto if raw.id <> invalid and raw.name <> invalid then out.Push(raw) return out end if end if return out end function sub forceClearSavedToken() m.token = "" sec = CreateObject("roRegistrySection", "awplayer") sec.Write("token", "") sec.Flush() end sub function isInvalidProviderMessage(msg as String) as Boolean if msg = invalid then return false s = LCase(msg) if s.InStr("provedor") >= 0 then return true if s.InStr("sess") >= 0 then return true if s.InStr("token") >= 0 then return true if s.InStr("inválid") >= 0 then return true if s.InStr("invalid") >= 0 then return true if s.InStr("sem retorno") >= 0 then return true if s.InStr("não autorizado") >= 0 then return true if s.InStr("nao autorizado") >= 0 then return true return false end function sub logoutToLogin(msg as String) forceClearSavedToken() showLoginScreen() if m.loginStatus <> invalid then if msg = invalid or msg = "" then msg = "Sessão encerrada. Faça login novamente." setLoginStatusSafe(msg) renderLoginScreen() end if end sub function awNormalizeAssetUrl(value as Dynamic) as String if value = invalid then return "" u = value.ToStr() if u = "" then return "" lower = LCase(u) ' Corrige links do Imgur para link direto aceito pelo Poster do Roku. if Instr(1, lower, "https://imgur.com/") = 1 then rest = Mid(u, 19) if rest <> "" then u = "https://i.imgur.com/" + rest else if Instr(1, lower, "http://imgur.com/") = 1 then rest = Mid(u, 18) if rest <> "" then u = "https://i.imgur.com/" + rest end if ' Se vier imgur sem extensão, tenta PNG. lower = LCase(u) if Instr(1, lower, "i.imgur.com/") > 0 then hasExt = false if Instr(1, lower, ".png") > 0 then hasExt = true if Instr(1, lower, ".jpg") > 0 then hasExt = true if Instr(1, lower, ".jpeg") > 0 then hasExt = true if Instr(1, lower, ".webp") > 0 then hasExt = true if hasExt = false then u = u + ".png" end if return u end function function awNowSecondsText() as String dt = CreateObject("roDateTime") return dt.AsSeconds().ToStr() end function function awAppendAssetsVersion(url as Dynamic) as String if url = invalid then return "" u = url.ToStr() if u = "" then return "" v = "" if m.assetsVersion <> invalid then v = m.assetsVersion.ToStr() if v = "" then v = awNowSecondsText() ' Cache-buster forte para o Roku não segurar fundo/logo/banner antigo. if Instr(1, u, "?") > 0 then return u + "&aw_v=" + v else return u + "?aw_v=" + v end if end function function awAppLogoUri() as String return "pkg:/images/logo.png" end function sub applyAppLogo() if m.homeLogo <> invalid then m.homeLogo.uri = awAppLogoUri() end if end sub function awHomeBackgroundUri() as String return "pkg:/images/webtv_theme_home_v251.jpg" end function sub applyHomeBackground() if m.awRedHomeBg = invalid then return m.awRedHomeBg.uri = awHomeBackgroundUri() end sub sub applyAppBackground() if m.appBackgroundImg = invalid then return m.appBackgroundImg.visible = true m.appBackgroundImg.uri = "pkg:/images/webtv_theme_internal_v251.jpg" end sub sub forceRefreshVisibleAssets() applyAppLogo() applyAppBackground() if m.homeLogo <> invalid then m.homeLogo.visible = true end if if m.mode = "login" then showStartupBanner() else if m.mode = "home" or m.mode = "home_start" then showHomeBanner() else hideStartupBanner() hideHomeBanner() end if end sub sub loadAppBanners() if m.modeBeforeBannerConfig = invalid or m.modeBeforeBannerConfig = "" then m.modeBeforeBannerConfig = m.mode end if m.mode = "banner_config" if m.apiGetTask <> invalid then m.apiGetTask.control = "STOP" m.apiGetTask.url = m.apiBase + "/api/roku/app-config.php?t=" + awNowSecondsText() m.apiGetTask.control = "RUN" end if end sub function awSafeInt(value as Dynamic, defaultValue as Integer) as Integer if value = invalid then return defaultValue s = value.ToStr() if s = invalid or s = "" then return defaultValue out = 0 found = false for i = 1 to Len(s) ch = Mid(s, i, 1) if ch >= "0" and ch <= "9" then out = (out * 10) + Val(ch) found = true end if end for if found = false then return defaultValue if out <= 0 then return defaultValue return out end function function awCfgInt(data as Dynamic, key as String, defaultValue as Integer) as Integer if data <> invalid then v = data.Lookup(key) if v <> invalid then return awSafeInt(v, defaultValue) end if return defaultValue end function function awCfgText(data as Dynamic, key as String, defaultValue as String) as String if data <> invalid then v = data.Lookup(key) if v <> invalid and v.ToStr() <> "" then return v.ToStr() end if return defaultValue end function function awCfgBool(data as Dynamic, key as String, defaultValue as Boolean) as Boolean if data = invalid then return defaultValue v = data.Lookup(key) if v = invalid then return defaultValue s = LCase(v.ToStr()) if s = "1" or s = "true" or s = "sim" or s = "yes" or s = "on" then return true if s = "0" or s = "false" or s = "nao" or s = "não" or s = "no" or s = "off" then return false return defaultValue end function function awColorFromName(name as Dynamic, defaultColor as String) as String if name = invalid then return defaultColor s = LCase(name.ToStr()) if Left(s, 2) = "0x" and Len(s) >= 10 then return name.ToStr() if s = "blue" or s = "azul" then return "0xDC2626FF" if s = "purple" or s = "roxo" then return "0xBE123CFF" if s = "green" or s = "verde" then return "0xDC2626FF" if s = "red" or s = "vermelho" then return "0xDC2626FF" if s = "yellow" or s = "amarelo" then return "0xF59E0BFF" return defaultColor end function sub applyDynamicTextsToScreen() if m.title <> invalid then m.title.text = m.appTitleText if m.heroTitle <> invalid then m.heroTitle.text = m.appTitleText if m.heroSub <> invalid then m.heroSub.text = m.appSubtitleText if m.loginTitle <> invalid then m.loginTitle.text = m.loginTitleText if m.mode = "activation" then if m.status <> invalid and m.status.text = "" then m.status.text = m.startupLoadingText if m.hint <> invalid and m.hint.text = "" then m.hint.text = m.startupHintText end if end sub function awOnlyDigits(txt as Dynamic) as String if txt = invalid then return "" s = txt.ToStr() digits = "" for i = 1 to Len(s) ch = Mid(s, i, 1) if ch >= "0" and ch <= "9" then digits = digits + ch end for return digits end function function awLastFiveDigits(txt as Dynamic) as String d = awOnlyDigits(txt) if Len(d) >= 5 then return Right(d, 5) while Len(d) < 5 d = "0" + d end while return d end function function awGetShortDeviceCode(deviceId as Dynamic) as String sec = CreateObject("roRegistrySection", "awplayer") if sec.Exists("short_device_code") then code = sec.Read("short_device_code") if code <> invalid and Len(code.ToStr()) = 5 then return code.ToStr() end if code = awLastFiveDigits(deviceId) if code = "00000" then dt = CreateObject("roDateTime") dt.Mark() code = awLastFiveDigits(dt.AsSeconds().ToStr()) end if sec.Write("short_device_code", code) sec.Flush() return code end function function awVisibleDeviceCode() as String if m.shortDeviceCode = invalid or m.shortDeviceCode = "" then m.shortDeviceCode = awGetShortDeviceCode(m.deviceId) end if return m.shortDeviceCode end function function awNormalizeBrandingText(txt as Dynamic) as String if txt = invalid then return "" s = txt.ToStr() if s = "" then return s s = s.Replace("AW PLAYER ROKU", "WEBTV") s = s.Replace("AW PLAY ROKU", "WEBTV") s = s.Replace("Aw Player Roku", "WebTV") s = s.Replace("AW Player Roku", "WebTV") s = s.Replace("AW Play Roku", "WebTV") s = s.Replace("AW PLAYER", "WEBTV") s = s.Replace("AW PLAY", "WEBTV") s = s.Replace("Aw Player", "WebTV") s = s.Replace("AW Player", "WebTV") s = s.Replace("Aw Play", "WebTV") s = s.Replace("AW Play", "WebTV") return s end function function awHasLegacyBranding(txt as Dynamic) as Boolean if txt = invalid then return false s = UCase(txt.ToStr()) if Instr(1, s, "AW PLAYER") > 0 then return true if Instr(1, s, "AW PLAY") > 0 then return true return false end function sub awForceAwPlayBranding(persist as Boolean) changed = false if m.appTitleText = invalid or m.appTitleText = "" or awHasLegacyBranding(m.appTitleText) then m.appTitleText = "WEBTV" changed = true else fixedTitle = awNormalizeBrandingText(m.appTitleText) if fixedTitle <> m.appTitleText then m.appTitleText = fixedTitle changed = true end if end if if m.loginTitleText = invalid or m.loginTitleText = "" or awHasLegacyBranding(m.loginTitleText) then m.loginTitleText = "WEBTV" changed = true else fixedLogin = awNormalizeBrandingText(m.loginTitleText) if fixedLogin <> m.loginTitleText then m.loginTitleText = fixedLogin changed = true end if end if if m.startupHintText = invalid or m.startupHintText = "" or awHasLegacyBranding(m.startupHintText) or Instr(1, LCase(m.startupHintText), "atlasplay") > 0 then m.startupHintText = "Cadastre o código em webapp.lat" changed = true else fixedHint = awNormalizeBrandingText(m.startupHintText) if fixedHint <> m.startupHintText then m.startupHintText = fixedHint changed = true end if end if if m.startupLoadingText <> invalid then fixedStart = awNormalizeBrandingText(m.startupLoadingText) if fixedStart <> m.startupLoadingText then m.startupLoadingText = fixedStart changed = true end if end if if m.loadingTitleText <> invalid then fixedLoadTitle = awNormalizeBrandingText(m.loadingTitleText) if fixedLoadTitle <> m.loadingTitleText then m.loadingTitleText = fixedLoadTitle changed = true end if end if if m.loadingSubText <> invalid then fixedLoadSub = awNormalizeBrandingText(m.loadingSubText) if fixedLoadSub <> m.loadingSubText then m.loadingSubText = fixedLoadSub changed = true end if end if if m.loadingLogoUrl <> invalid and m.loadingLogoUrl <> "" then m.loadingLogoUrl = "" changed = true end if if persist and changed then sec = CreateObject("roRegistrySection", "awplayer") sec.Write("app_title", m.appTitleText) sec.Write("login_title_text", m.loginTitleText) sec.Write("startup_hint_text", m.startupHintText) sec.Write("startup_loading_text", m.startupLoadingText) sec.Write("loading_title_text", m.loadingTitleText) sec.Write("loading_sub_text", m.loadingSubText) sec.Write("loading_logo_url", "") sec.Flush() end if end sub sub loadSavedAssetConfig() sec = CreateObject("roRegistrySection", "awplayer") if sec.Exists("assets_version") then m.assetsVersion = sec.Read("assets_version") if sec.Exists("app_version_display") then m.appVersionDisplay = sec.Read("app_version_display") if sec.Exists("logo_url") then m.logoUrl = awNormalizeAssetUrl(sec.Read("logo_url")) if sec.Exists("background_url") then m.backgroundUrl = awNormalizeAssetUrl(sec.Read("background_url")) if sec.Exists("home_background_url") then m.homeBackgroundUrl = awNormalizeAssetUrl(sec.Read("home_background_url")) if sec.Exists("startup_banner") then m.startupBannerUrl = awNormalizeAssetUrl(sec.Read("startup_banner")) if sec.Exists("home_banner") then m.homeBannerUrl = awNormalizeAssetUrl(sec.Read("home_banner")) if sec.Exists("app_title") then m.appTitleText = sec.Read("app_title") if sec.Exists("app_subtitle") then m.appSubtitleText = sec.Read("app_subtitle") if sec.Exists("startup_loading_text") then m.startupLoadingText = sec.Read("startup_loading_text") if sec.Exists("startup_hint_text") then m.startupHintText = sec.Read("startup_hint_text") if sec.Exists("login_title_text") then m.loginTitleText = sec.Read("login_title_text") if sec.Exists("login_info_text") then m.loginInfoText = sec.Read("login_info_text") if sec.Exists("home_live_text") then m.homeLiveText = sec.Read("home_live_text") if sec.Exists("home_movies_text") then m.homeMoviesText = sec.Read("home_movies_text") if sec.Exists("home_series_text") then m.homeSeriesText = sec.Read("home_series_text") if sec.Exists("home_list_text") then m.homeListText = sec.Read("home_list_text") if sec.Exists("home_settings_text") then m.homeSettingsText = sec.Read("home_settings_text") if sec.Exists("home_reload_text") then m.homeReloadText = sec.Read("home_reload_text") if sec.Exists("home_expiration_prefix") then m.homeExpirationPrefix = sec.Read("home_expiration_prefix") if sec.Exists("list_title_text") then m.listTitleText = sec.Read("list_title_text") if sec.Exists("list_username_label") then m.listUsernameLabel = sec.Read("list_username_label") if sec.Exists("list_expiration_label") then m.listExpirationLabel = sec.Read("list_expiration_label") if sec.Exists("list_device_label") then m.listDeviceLabel = sec.Read("list_device_label") if sec.Exists("list_version_label") then m.listVersionLabel = sec.Read("list_version_label") if sec.Exists("list_change_login_text") then m.listChangeLoginText = sec.Read("list_change_login_text") if sec.Exists("list_delete_text") then m.listDeleteText = sec.Read("list_delete_text") if sec.Exists("list_back_text") then m.listBackText = sec.Read("list_back_text") if sec.Exists("login_provider_label") then m.loginProviderLabelText = sec.Read("login_provider_label") if sec.Exists("login_user_label") then m.loginUserLabelText = sec.Read("login_user_label") if sec.Exists("login_password_label") then m.loginPasswordLabelText = sec.Read("login_password_label") if sec.Exists("login_button_text") then m.loginButtonTextValue = sec.Read("login_button_text") if sec.Exists("login_refresh_text") then m.loginRefreshTextValue = sec.Read("login_refresh_text") if sec.Exists("login_keyboard_done_text") then m.loginKeyboardDoneText = sec.Read("login_keyboard_done_text") if sec.Exists("login_keyboard_clear_text") then m.loginKeyboardClearText = sec.Read("login_keyboard_clear_text") if sec.Exists("login_keyboard_space_text") then m.loginKeyboardSpaceText = sec.Read("login_keyboard_space_text") if sec.Exists("login_keyboard_back_text") then m.loginKeyboardBackText = sec.Read("login_keyboard_back_text") if sec.Exists("loading_logo_url") then m.loadingLogoUrl = awNormalizeAssetUrl(sec.Read("loading_logo_url")) if sec.Exists("loading_title_text") then m.loadingTitleText = sec.Read("loading_title_text") if sec.Exists("loading_sub_text") then m.loadingSubText = sec.Read("loading_sub_text") if sec.Exists("loading_style") then m.loadingStyle = sec.Read("loading_style") if sec.Exists("player_seek_movie_seconds") then m.playerSeekMovieSeconds = awSafeInt(sec.Read("player_seek_movie_seconds"), 30) if sec.Exists("player_seek_series_seconds") then m.playerSeekSeriesSeconds = awSafeInt(sec.Read("player_seek_series_seconds"), 300) if sec.Exists("next_episode_enabled") then m.nextEpisodeEnabled = (sec.Read("next_episode_enabled") <> "0") if sec.Exists("next_episode_prompt_seconds") then m.nextEpisodePromptSeconds = awSafeInt(sec.Read("next_episode_prompt_seconds"), 60) if sec.Exists("next_episode_button_text") then m.nextEpisodeButtonLabel = sec.Read("next_episode_button_text") if sec.Exists("next_episode_button_color") then m.nextEpisodeButtonColor = awColorFromName(sec.Read("next_episode_button_color"), "0xDC2626FF") awForceAwPlayBranding(true) forceRefreshVisibleAssets() applyDynamicTextsToScreen() end sub sub applyBannerConfig(data as Dynamic) if data = invalid then return if data.assets_version <> invalid then m.assetsVersion = data.assets_version.ToStr() m.appVersionDisplay = awCfgText(data, "app_version_display", m.appVersionDisplay) if data.logo_url <> invalid then m.logoUrl = awNormalizeAssetUrl(data.logo_url) if data.background_url <> invalid then m.backgroundUrl = awNormalizeAssetUrl(data.background_url) if data.home_background_url <> invalid then m.homeBackgroundUrl = awNormalizeAssetUrl(data.home_background_url) if data.startup_banner <> invalid then m.startupBannerUrl = awNormalizeAssetUrl(data.startup_banner) if data.home_banner <> invalid then m.homeBannerUrl = awNormalizeAssetUrl(data.home_banner) if data.app_banner <> invalid and (m.homeBannerUrl = invalid or m.homeBannerUrl = "") then m.homeBannerUrl = awNormalizeAssetUrl(data.app_banner) end if m.appTitleText = awCfgText(data, "app_title", m.appTitleText) m.appSubtitleText = awCfgText(data, "app_subtitle", m.appSubtitleText) m.startupLoadingText = awCfgText(data, "startup_loading_text", m.startupLoadingText) m.startupHintText = awCfgText(data, "startup_hint_text", m.startupHintText) oldLoginTitle = m.loginTitleText m.loginTitleText = awCfgText(data, "login_title_text", m.loginTitleText) if (m.loginTitleText = oldLoginTitle or m.loginTitleText = invalid or m.loginTitleText = "") and m.appTitleText <> invalid and m.appTitleText <> "" then m.loginTitleText = m.appTitleText m.loginInfoText = awCfgText(data, "login_info_text", m.loginInfoText) m.homeLiveText = awCfgText(data, "home_live_text", m.homeLiveText) m.homeMoviesText = awCfgText(data, "home_movies_text", m.homeMoviesText) m.homeSeriesText = awCfgText(data, "home_series_text", m.homeSeriesText) m.homeListText = awCfgText(data, "home_list_text", m.homeListText) m.homeSettingsText = awCfgText(data, "home_settings_text", m.homeSettingsText) m.homeReloadText = awCfgText(data, "home_reload_text", m.homeReloadText) m.homeExpirationPrefix = awCfgText(data, "home_expiration_prefix", m.homeExpirationPrefix) m.listTitleText = awCfgText(data, "list_title_text", m.listTitleText) m.listUsernameLabel = awCfgText(data, "list_username_label", m.listUsernameLabel) m.listExpirationLabel = awCfgText(data, "list_expiration_label", m.listExpirationLabel) m.listDeviceLabel = awCfgText(data, "list_device_label", m.listDeviceLabel) m.listVersionLabel = awCfgText(data, "list_version_label", m.listVersionLabel) m.listChangeLoginText = awCfgText(data, "list_change_login_text", m.listChangeLoginText) m.listDeleteText = awCfgText(data, "list_delete_text", m.listDeleteText) m.listBackText = awCfgText(data, "list_back_text", m.listBackText) m.loginProviderLabelText = awCfgText(data, "login_provider_label", m.loginProviderLabelText) m.loginUserLabelText = awCfgText(data, "login_user_label", m.loginUserLabelText) m.loginPasswordLabelText = awCfgText(data, "login_password_label", m.loginPasswordLabelText) m.loginButtonTextValue = awCfgText(data, "login_button_text", m.loginButtonTextValue) m.loginRefreshTextValue = awCfgText(data, "login_refresh_text", m.loginRefreshTextValue) m.loginKeyboardDoneText = awCfgText(data, "login_keyboard_done_text", m.loginKeyboardDoneText) m.loginKeyboardClearText = awCfgText(data, "login_keyboard_clear_text", m.loginKeyboardClearText) m.loginKeyboardSpaceText = awCfgText(data, "login_keyboard_space_text", m.loginKeyboardSpaceText) m.loginKeyboardBackText = awCfgText(data, "login_keyboard_back_text", m.loginKeyboardBackText) if data.loading_logo_url <> invalid then m.loadingLogoUrl = awNormalizeAssetUrl(data.loading_logo_url) m.loadingTitleText = awCfgText(data, "loading_title_text", m.loadingTitleText) m.loadingSubText = awCfgText(data, "loading_sub_text", m.loadingSubText) m.loadingStyle = awCfgText(data, "loading_style", m.loadingStyle) if m.loadingStyle = "" then m.loadingStyle = "static" m.playerSeekMovieSeconds = awCfgInt(data, "player_seek_movie_seconds", m.playerSeekMovieSeconds) m.playerSeekSeriesSeconds = awCfgInt(data, "player_seek_series_seconds", m.playerSeekSeriesSeconds) m.nextEpisodeEnabled = awCfgBool(data, "next_episode_enabled", m.nextEpisodeEnabled) m.nextEpisodePromptSeconds = awCfgInt(data, "next_episode_prompt_seconds", m.nextEpisodePromptSeconds) if m.nextEpisodePromptSeconds < 15 then m.nextEpisodePromptSeconds = 60 m.nextEpisodeButtonLabel = awCfgText(data, "next_episode_button_text", m.nextEpisodeButtonLabel) m.nextEpisodeButtonColor = awColorFromName(awCfgText(data, "next_episode_button_color", m.nextEpisodeButtonColor), "0xDC2626FF") m.logoUrl = "" m.backgroundUrl = "" m.homeBackgroundUrl = "" m.loadingLogoUrl = "" m.loadingStyle = "awplay" awForceAwPlayBranding(false) ' Salva a aparência para carregar mesmo se a API demorar na próxima abertura sec = CreateObject("roRegistrySection", "awplayer") if m.assetsVersion <> invalid then sec.Write("assets_version", m.assetsVersion) if m.appVersionDisplay <> invalid then sec.Write("app_version_display", m.appVersionDisplay) if m.logoUrl <> invalid then sec.Write("logo_url", m.logoUrl) if m.backgroundUrl <> invalid then sec.Write("background_url", m.backgroundUrl) if m.homeBackgroundUrl <> invalid then sec.Write("home_background_url", m.homeBackgroundUrl) if m.startupBannerUrl <> invalid then sec.Write("startup_banner", m.startupBannerUrl) if m.homeBannerUrl <> invalid then sec.Write("home_banner", m.homeBannerUrl) if m.appTitleText <> invalid then sec.Write("app_title", m.appTitleText) if m.appSubtitleText <> invalid then sec.Write("app_subtitle", m.appSubtitleText) if m.startupLoadingText <> invalid then sec.Write("startup_loading_text", m.startupLoadingText) if m.startupHintText <> invalid then sec.Write("startup_hint_text", m.startupHintText) if m.loginTitleText <> invalid then sec.Write("login_title_text", m.loginTitleText) if m.loginInfoText <> invalid then sec.Write("login_info_text", m.loginInfoText) if m.homeLiveText <> invalid then sec.Write("home_live_text", m.homeLiveText) if m.homeMoviesText <> invalid then sec.Write("home_movies_text", m.homeMoviesText) if m.homeSeriesText <> invalid then sec.Write("home_series_text", m.homeSeriesText) if m.homeListText <> invalid then sec.Write("home_list_text", m.homeListText) if m.homeSettingsText <> invalid then sec.Write("home_settings_text", m.homeSettingsText) if m.homeReloadText <> invalid then sec.Write("home_reload_text", m.homeReloadText) if m.homeExpirationPrefix <> invalid then sec.Write("home_expiration_prefix", m.homeExpirationPrefix) if m.listTitleText <> invalid then sec.Write("list_title_text", m.listTitleText) if m.listUsernameLabel <> invalid then sec.Write("list_username_label", m.listUsernameLabel) if m.listExpirationLabel <> invalid then sec.Write("list_expiration_label", m.listExpirationLabel) if m.listDeviceLabel <> invalid then sec.Write("list_device_label", m.listDeviceLabel) if m.listVersionLabel <> invalid then sec.Write("list_version_label", m.listVersionLabel) if m.listChangeLoginText <> invalid then sec.Write("list_change_login_text", m.listChangeLoginText) if m.listDeleteText <> invalid then sec.Write("list_delete_text", m.listDeleteText) if m.listBackText <> invalid then sec.Write("list_back_text", m.listBackText) if m.loginProviderLabelText <> invalid then sec.Write("login_provider_label", m.loginProviderLabelText) if m.loginUserLabelText <> invalid then sec.Write("login_user_label", m.loginUserLabelText) if m.loginPasswordLabelText <> invalid then sec.Write("login_password_label", m.loginPasswordLabelText) if m.loginButtonTextValue <> invalid then sec.Write("login_button_text", m.loginButtonTextValue) if m.loginRefreshTextValue <> invalid then sec.Write("login_refresh_text", m.loginRefreshTextValue) if m.loginKeyboardDoneText <> invalid then sec.Write("login_keyboard_done_text", m.loginKeyboardDoneText) if m.loginKeyboardClearText <> invalid then sec.Write("login_keyboard_clear_text", m.loginKeyboardClearText) if m.loginKeyboardSpaceText <> invalid then sec.Write("login_keyboard_space_text", m.loginKeyboardSpaceText) if m.loginKeyboardBackText <> invalid then sec.Write("login_keyboard_back_text", m.loginKeyboardBackText) if m.loadingLogoUrl <> invalid then sec.Write("loading_logo_url", m.loadingLogoUrl) if m.loadingTitleText <> invalid then sec.Write("loading_title_text", m.loadingTitleText) if m.loadingSubText <> invalid then sec.Write("loading_sub_text", m.loadingSubText) if m.loadingStyle <> invalid then sec.Write("loading_style", m.loadingStyle) if m.playerSeekMovieSeconds <> invalid then sec.Write("player_seek_movie_seconds", m.playerSeekMovieSeconds.ToStr()) if m.playerSeekSeriesSeconds <> invalid then sec.Write("player_seek_series_seconds", m.playerSeekSeriesSeconds.ToStr()) if m.nextEpisodeEnabled = true then sec.Write("next_episode_enabled", "1") else sec.Write("next_episode_enabled", "0") if m.nextEpisodePromptSeconds <> invalid then sec.Write("next_episode_prompt_seconds", m.nextEpisodePromptSeconds.ToStr()) if m.nextEpisodeButtonLabel <> invalid then sec.Write("next_episode_button_text", m.nextEpisodeButtonLabel) if m.nextEpisodeButtonColor <> invalid then sec.Write("next_episode_button_color", m.nextEpisodeButtonColor) sec.Flush() forceRefreshVisibleAssets() applyDynamicTextsToScreen() end sub sub showStartupBanner() if m.startupBannerImg = invalid then return if m.startupBannerUrl <> invalid and m.startupBannerUrl <> "" then m.startupBannerImg.uri = awAppendAssetsVersion(m.startupBannerUrl) m.startupBannerImg.visible = true else m.startupBannerImg.visible = false end if end sub sub hideStartupBanner() if m.startupBannerImg <> invalid then m.startupBannerImg.visible = false end sub sub showHomeBanner() if m.homeBannerImg = invalid then return if m.homeBannerUrl <> invalid and m.homeBannerUrl <> "" then m.homeBannerImg.uri = awAppendAssetsVersion(m.homeBannerUrl) m.homeBannerImg.visible = true else m.homeBannerImg.visible = false end if end sub sub hideHomeBanner() if m.homeBannerImg <> invalid then m.homeBannerImg.visible = false end sub sub hideLoginUi() if m.loginGroup <> invalid then m.loginGroup.visible = false if m.loginKeyboardGroup <> invalid then m.loginKeyboardGroup.visible = false if m.listInfoGroup <> invalid then m.listInfoGroup.visible = false if m.footer <> invalid then m.footer.visible = true end sub sub hideHomeLatestRows() ' Compatibilidade: base v1.6.8 não usa fileira de últimos filmes na Home. end sub sub requestRokuUserDataForSignIn() ' Exigência Roku para apps autenticados: abrir a tela Request For Information (RFI). if m.channelStore = invalid then return if m.rokuUserDataRequested = true then return m.rokuUserDataRequested = true info = CreateObject("roSGNode", "ContentNode") if info <> invalid then info.addFields({ context: "signin" }) m.channelStore.requestedUserDataInfo = info end if m.channelStore.requestedUserData = "email" m.channelStore.command = "getUserData" end sub sub onRokuUserDataReceived() if m.channelStore = invalid then return data = m.channelStore.userData if data <> invalid then if data.email <> invalid then m.rokuUserEmail = data.email.ToStr() ' Se o usuário do provedor também for e-mail, já deixa preenchido. if m.loginUsername = invalid or m.loginUsername = "" then m.loginUsername = m.rokuUserEmail end if if m.mode = "login" then renderLoginScreen() end if end if end sub sub showLoginScreen() hideStartupAnimation() hideSportsScreen() hideListInfoUi() applyAppBackground() hideActivationText() hideHomeBanner() showStartupBanner() hideAllHomeRows() hideHomeLatestRows() setPremiumHomeVisible(false) clearMovieUi() ' Logo e banner controlados pelo painel também aparecem no início/login. applyAppLogo() if m.homeLogo <> invalid then m.homeLogo.width = 135 m.homeLogo.height = 92 m.homeLogo.translation = [572, 18] m.homeLogo.visible = true end if showStartupBanner() if m.loginGroup = invalid then return applyDynamicTextsToScreen() m.mode = "login" m.loginField = 0 m.loginProviderCode = "" m.loginUsername = "" m.loginPassword = "" m.loginStatus.text = m.loginInfoText m.loginGroup.visible = true if m.loginKeyboardGroup <> invalid then m.loginKeyboardGroup.visible = false if m.footer <> invalid then m.footer.visible = false devLbl = m.top.findNode("loginDeviceKeyLabel") if devLbl <> invalid then devText = "--" devText = awVisibleDeviceCode() devLbl.text = m.listDeviceLabel + " " + devText end if verLbl = m.top.findNode("loginVersionLabel") if verLbl <> invalid then verLbl.text = m.listVersionLabel + " " + m.appVersionDisplay renderLoginScreen() ' Chamada RFI Roku exigida na certificação para fluxo de login. requestRokuUserDataForSignIn() ' v1.8.39: não carregar banners/config na tela de login. ' Isso evitava HTTP -3 antes do usuário tentar entrar. ' login_screen_clear_http3_v138 if m.loginStatus <> invalid then if Instr(1, LCase(m.loginStatus.text), "http -3") > 0 or Instr(1, LCase(m.loginStatus.text), "http 3") > 0 then m.loginStatus.text = "" end if end if end sub sub renderLoginScreen() if m.loginGroup = invalid then return m.loginGroup.visible = true if m.loginKeyboardGroup <> invalid then m.loginKeyboardGroup.visible = false if m.loginProviderCode = invalid then m.loginProviderCode = "" if m.loginDnsLabel <> invalid then m.loginDnsLabel.text = m.loginProviderLabelText if m.loginUserLabel <> invalid then m.loginUserLabel.text = m.loginUserLabelText if m.loginPassLabel <> invalid then m.loginPassLabel.text = m.loginPasswordLabelText if m.loginButtonTextNode <> invalid then m.loginButtonTextNode.text = m.loginButtonTextValue if m.loginRefreshButtonTextNode <> invalid then m.loginRefreshButtonTextNode.text = m.loginRefreshTextValue m.loginDnsValue.text = m.loginProviderCode if m.loginProviderCode = "" then m.loginDnsValue.text = m.loginProviderLabelText m.loginUserValue.text = m.loginUsername stars = "" for i = 1 to Len(m.loginPassword) stars = stars + "*" end for m.loginPassValue.text = stars m.loginDnsBox.color = "0x140D10FF" m.loginUserBox.color = "0x140D10FF" m.loginPassBox.color = "0x140D10FF" m.loginButtonBox.color = "0xB91C1CFF" if m.loginRefreshButtonBox <> invalid then m.loginRefreshButtonBox.color = "0x140D10FF" if m.loginField = 0 then m.loginDnsBox.color = "0xDC2626FF" else if m.loginField = 1 then m.loginUserBox.color = "0xDC2626FF" else if m.loginField = 2 then m.loginPassBox.color = "0xDC2626FF" else if m.loginField = 3 then m.loginButtonBox.color = "0xEF4444FF" else if m.loginField = 4 then if m.loginRefreshButtonBox <> invalid then m.loginRefreshButtonBox.color = "0xDC2626FF" end if end sub function loginCurrentDnsId() as Integer if m.loginDnsList = invalid then return 0 if Type(m.loginDnsList) <> "roArray" then m.loginDnsList = awDnsToArray(m.loginDnsList) if m.loginDnsList.Count() = 0 then return 0 if m.loginDnsIndex < 0 then m.loginDnsIndex = 0 if m.loginDnsIndex >= m.loginDnsList.Count() then m.loginDnsIndex = 0 d = m.loginDnsList[m.loginDnsIndex] if d <> invalid and d.id <> invalid then return d.id return 0 end function sub ensureLoginDynKeyboard() if m.loginKeyboardGroup = invalid then return if m.loginDynKeyboard = invalid then kb = CreateObject("roSGNode", "LoginKeyboard") if kb <> invalid then kb.translation = [112, 455] kb.visible = false kb.observeField("text", "onLoginDynKeyboardTextChanged") kb.observeField("continue", "onLoginDynKeyboardContinue") m.loginKeyboardGroup.appendChild(kb) m.loginDynKeyboard = kb end if end if end sub sub destroyLoginDynKeyboardFocus() if m.loginDynKeyboard <> invalid then m.loginDynKeyboard.visible = false m.loginDynKeyboard.continue = false m.loginDynKeyboard.setFocus(false) if m.loginKeyboardGroup <> invalid then m.loginKeyboardGroup.removeChild(m.loginDynKeyboard) end if m.loginDynKeyboard = invalid end if end sub sub startLoginKeyboard(fieldName as String) ' v1.8.223: teclado ZPLAY recriado a cada abertura para não prender foco escondido. ensureLoginDynKeyboard() configureKeyboardForLogin() m.mode = "login_keyboard" m.loginKeyboardField = fieldName if fieldName = "provider" then m.loginKeyboardText = m.loginProviderCode if m.loginKeyboardTitle <> invalid then m.loginKeyboardTitle.text = "DIGITANDO: " + m.loginProviderLabelText else if fieldName = "username" then m.loginKeyboardText = m.loginUsername if m.loginKeyboardTitle <> invalid then m.loginKeyboardTitle.text = "DIGITANDO: " + m.loginUserLabelText else m.loginKeyboardText = m.loginPassword if m.loginKeyboardTitle <> invalid then m.loginKeyboardTitle.text = "DIGITANDO: " + m.loginPasswordLabelText end if if m.loginGroup <> invalid then m.loginGroup.visible = true if m.loginKeyboardGroup <> invalid then m.loginKeyboardGroup.visible = true if m.footer <> invalid then m.footer.visible = false if m.loginDynKeyboard <> invalid then m.loginDynKeyboard.purpose = "login" m.loginDynKeyboard.mode = "ABC123Lower" m.loginDynKeyboard.text = m.loginKeyboardText m.loginDynKeyboard.visible = true m.loginDynKeyboard.setFocus(true) end if applyLoginKeyboardLivePreview() renderLoginKeyboard() end sub sub onLoginDynKeyboardTextChanged() if m.loginDynKeyboard = invalid then return if m.mode = "movie_search_keyboard" and m.searchKeyboardStyle = "zplay" then newSearchText = m.loginDynKeyboard.text if newSearchText = invalid then newSearchText = "" changedSearch = (newSearchText <> m.searchQuery) m.searchQuery = newSearchText m.loginKeyboardText = m.searchQuery renderMovieLiveSearch() if changedSearch then scheduleMovieLiveSearch() return end if if m.mode <> "login_keyboard" then return m.loginKeyboardText = m.loginDynKeyboard.text if m.loginKeyboardText = invalid then m.loginKeyboardText = "" ' v1.8.233: mostra cada caractere imediatamente no campo original do login. applyLoginKeyboardLivePreview() renderLoginKeyboard() end sub sub applyLoginKeyboardLivePreview() if m.mode <> "login_keyboard" then return txt = m.loginKeyboardText if txt = invalid then txt = "" if m.loginKeyboardField = "provider" then m.loginProviderCode = txt if m.loginDnsValue <> invalid then if txt = "" then m.loginDnsValue.text = m.loginProviderLabelText else m.loginDnsValue.text = txt end if end if if m.loginDnsBox <> invalid then m.loginDnsBox.color = "0xDC2626FF" else if m.loginKeyboardField = "username" then m.loginUsername = txt if m.loginUserValue <> invalid then m.loginUserValue.text = txt if m.loginUserBox <> invalid then m.loginUserBox.color = "0xDC2626FF" else if m.loginKeyboardField = "password" then m.loginPassword = txt stars = "" for i = 1 to Len(txt) stars = stars + "*" end for if m.loginPassValue <> invalid then m.loginPassValue.text = stars if m.loginPassBox <> invalid then m.loginPassBox.color = "0xDC2626FF" end if end sub sub onLoginDynKeyboardContinue() if m.loginDynKeyboard = invalid then return if m.loginDynKeyboard.continue = true then m.loginDynKeyboard.continue = false if m.mode = "movie_search_keyboard" and m.searchKeyboardStyle = "zplay" then finishSearchZplayKeyboard(true) else if m.mode = "login_keyboard" then finishLoginKeyboard() end if end if end sub sub onLoginDialogButtonSelected() if m.loginInputDialog = invalid then return selected = m.loginInputDialog.buttonSelected if selected = 0 then m.loginDialogAccepted = true saveLoginDialogText() end if m.loginInputDialog.close = true end sub sub onLoginDialogClosed() if m.loginInputDialog <> invalid then if m.loginDialogAccepted = true then saveLoginDialogText() end if end if m.loginInputDialog = invalid m.mode = "login" if m.loginGroup <> invalid then m.loginGroup.visible = true if m.loginKeyboardGroup <> invalid then m.loginKeyboardGroup.visible = false if m.footer <> invalid then m.footer.visible = false renderLoginScreen() end sub sub saveLoginDialogText() if m.loginInputDialog = invalid then return txt = m.loginInputDialog.text if txt = invalid then txt = "" if m.loginKeyboardField = "provider" then m.loginProviderCode = txt else if m.loginKeyboardField = "username" then m.loginUsername = txt else if m.loginKeyboardField = "password" then m.loginPassword = txt end if end sub sub renderLoginKeyboard() if m.mode = "movie_search_keyboard" and m.searchKeyboardStyle = "zplay" then if m.loginGroup <> invalid then m.loginGroup.visible = false else if m.loginGroup <> invalid then m.loginGroup.visible = true end if if m.loginKeyboardGroup <> invalid then m.loginKeyboardGroup.visible = true if m.footer <> invalid then m.footer.visible = false displayText = m.loginKeyboardText if displayText = invalid then displayText = "" if m.loginKeyboardField = "password" then displayText = "" for i = 1 to Len(m.loginKeyboardText) displayText = displayText + "*" end for end if if m.loginKeyboardValue <> invalid then m.loginKeyboardValue.text = displayText ' Esconde as teclas manuais antigas para usar somente o teclado importado. if m.loginKeys <> invalid then for each k in m.loginKeys if k <> invalid then k.visible = false end for end if if m.loginDynKeyboard <> invalid then m.loginDynKeyboard.visible = true if m.loginDynKeyboard.text <> m.loginKeyboardText then m.loginDynKeyboard.text = m.loginKeyboardText end if end sub sub finishLoginKeyboard() if m.loginDynKeyboard <> invalid then m.loginKeyboardText = m.loginDynKeyboard.text end if if m.loginKeyboardText = invalid then m.loginKeyboardText = "" nextField = m.loginField if m.loginKeyboardField = "provider" then m.loginProviderCode = m.loginKeyboardText nextField = 1 else if m.loginKeyboardField = "username" then m.loginUsername = m.loginKeyboardText nextField = 2 else m.loginPassword = m.loginKeyboardText nextField = 3 end if ' Remove o DynamicCustomKeyboard depois de usar. ' Isso evita ele ficar invisível segurando o foco e travando a tela de login. destroyLoginDynKeyboardFocus() m.mode = "login" m.loginField = nextField if m.loginKeyboardGroup <> invalid then m.loginKeyboardGroup.visible = false if m.loginGroup <> invalid then m.loginGroup.visible = true if m.footer <> invalid then m.footer.visible = false renderLoginScreen() if m.loginKeyboardGroup <> invalid then m.loginKeyboardGroup.visible = false if m.top <> invalid then m.top.setFocus(true) if m.loginFocusTimer <> invalid then m.loginFocusTimer.control = "stop" m.loginFocusTimer.control = "start" end if end sub sub onLoginFocusTimer() if m.mode <> "login" then return if m.loginKeyboardGroup <> invalid then m.loginKeyboardGroup.visible = false if m.loginDynKeyboard <> invalid then m.loginDynKeyboard.visible = false m.loginDynKeyboard.continue = false m.loginDynKeyboard.setFocus(false) end if if m.loginGroup <> invalid then m.loginGroup.visible = true if m.footer <> invalid then m.footer.visible = false renderLoginScreen() if m.top <> invalid then m.top.setFocus(true) end sub sub refreshLoginConfig() if m.loginStatus <> invalid then m.loginStatus.text = "Atualizando dados do painel..." m.modeBeforeBannerConfig = "login" loadAppBanners() end sub sub loginSubmit() if m.loginProviderCode = invalid or m.loginProviderCode = "" then m.loginStatus.text = "Digite o código do provedor." m.loginField = 0 renderLoginScreen() return end if if m.loginUsername = "" then m.loginStatus.text = "Digite o usuário." m.loginField = 1 renderLoginScreen() return end if if m.loginPassword = "" then m.loginStatus.text = "Digite a senha." m.loginField = 2 renderLoginScreen() return end if m.loginStatus.text = "Validando login..." payload = { provider_code: m.loginProviderCode, username: m.loginUsername, password: m.loginPassword, device_id: m.deviceId, device_code: awVisibleDeviceCode(), device_name: "Roku", app_version: m.appVersionDisplay } m.apiPostTask.url = m.apiBase + "/api/roku/login.php" m.apiPostTask.body = FormatJson(payload) m.apiPostTask.control = "RUN" end sub sub saveLoginSessionSafe(res as Dynamic) if res = invalid then return token = "" if res.token <> invalid then token = res.token.ToStr() if token = "" and m.token <> invalid then token = m.token.ToStr() if token = "" then return sec = CreateObject("roRegistrySection", "awplayer") sec.Write("token", token) if m.loginProviderCode <> invalid then sec.Write("provider_code", m.loginProviderCode.ToStr()) if m.loginUsername <> invalid then sec.Write("username", m.loginUsername.ToStr()) if m.loginPassword <> invalid then sec.Write("password", m.loginPassword.ToStr()) if res.dns <> invalid then if res.dns.id <> invalid then sec.Write("dns_id", res.dns.id.ToStr()) if res.dns.name <> invalid then sec.Write("dns_name", res.dns.name.ToStr()) if res.dns.base_url <> invalid then sec.Write("dns_base_url", res.dns.base_url.ToStr()) end if if res.user_info <> invalid then if res.user_info.username <> invalid then sec.Write("username", res.user_info.username.ToStr()) if res.user_info.password <> invalid then sec.Write("password", res.user_info.password.ToStr()) end if expiryRaw = awFindExpiryRaw(res) if expiryRaw <> invalid and expiryRaw <> "" then sec.Write("expiry_raw", expiryRaw) end if sec.Flush() m.token = token end sub sub onApiPostResult() res = m.apiPostTask.result ' onApiPostResult_login_guard_v138 if res <> invalid and isLoginModeActive() then if res.success <> invalid and res.success <> true then if res.message <> invalid then showLoginFriendlyError(res.message) return else if res.error <> invalid then showLoginFriendlyError(res.error) return end if end if end if if res = invalid then if isLoginModeActive() then showLoginFriendlyError("Sem retorno da API.") return end if ' save_login_v140_guard if res <> invalid and res.success <> invalid and res.success = true then if res.token <> invalid then saveLoginSessionSafe(res) end if end if if m.mode = "validating_token" or m.mode = "checking_device" or m.mode = "live_categories" or m.mode = "live_streams" then forceLoginFromApiError("Sem retorno da API. Faça login novamente.") return end if else if res.success <> invalid and res.success <> true then msg = "" if res.message <> invalid then msg = res.message.ToStr() if shouldForceLoginForApiError() and isAwSessionInvalidMessage(msg) then forceLoginFromApiError(msg) return end if else if res.error <> invalid then msg = res.error.ToStr() if shouldForceLoginForApiError() and isAwSessionInvalidMessage(msg) then forceLoginFromApiError(msg) return end if end if if res = invalid then if m.mode = "login" then m.loginStatus.text = "Erro no login. Sem resposta." renderLoginScreen() end if return end if if m.mode = "login" then if res.success = true then data = res.data if data = invalid then data = res loginSuccess(data) else msg = "Login inválido" if res.message <> invalid then msg = res.message setLoginStatusSafe(msg) renderLoginScreen() end if return end if end sub sub loginSuccess(data as Dynamic) token = "" if data <> invalid and data.token <> invalid then token = data.token.ToStr() if token = "" then m.loginStatus.text = "Login OK, mas token não retornou." renderLoginScreen() return end if sec = CreateObject("roRegistrySection", "awplayer") sec.Write("token", token) sec.Flush() saveLoginSessionSafe(data) applyBannerConfig(data) m.bannerConfigLoaded = true m.token = token hideLoginUi() hideStartupBanner() showHome() end sub sub showHome() hideStartupAnimation() hideSportsScreen() if m.bannerConfigLoaded <> true then m.bannerConfigLoaded = true end if hideLoginUi() hideStartupBanner() hideActivationText() if m.title <> invalid then m.title.visible = false if m.title <> invalid then m.title.text = "" hideAllPosterOverlayBadges() hideAllHomeRows() hideHomeBanner() clearMovieUi() hideChannelLiveUiOnly() m.mode = "home" m.homeArea = "cards" m.menuIndex = 0 m.page = 0 setPremiumHomeVisible(true) renderPremiumHome() loadHomeInfoForExpiration() m.footer.text = "" end sub sub clearHome() hideActivationText() hideSportsScreen() hideChannelLiveUiOnly() hideStartupBanner() hideHomeBanner() hideKeyboard() hideAllPosterOverlayBadges() hideAllHomeRows() m.menuTitle.text = "" m.menu1.text = "" m.menu2.text = "" m.menu3.text = "" m.menu4.text = "" m.menu5.text = "" m.menu6.text = "" m.menu7.text = "" if m.expiryLabel <> invalid then m.expiryLabel.visible = false if m.expiryBg <> invalid then m.expiryBg.visible = false if m.playerOverlay <> invalid then m.playerOverlay.visible = false if m.playerTitle <> invalid then m.playerTitle.visible = false if m.playerHint <> invalid then m.playerHint.visible = false if m.playerEpg <> invalid then m.playerEpg.visible = false end sub sub renderList() if m.mode = "live_streams" or m.mode = "favorites" then renderChannelStreamsModern() return end if hideAllHomeRows() hideAllPosterOverlayBadges() clearHome() ' renderListGenericLabelsVisibleV63 labels = [m.menu1, m.menu2, m.menu3, m.menu4, m.menu5, m.menu6, m.menu7] for each lb in labels if lb <> invalid then lb.visible = true end for if m.menuTitle <> invalid then m.menuTitle.visible = true startIndex = m.page * 7 totalPages = 1 if m.items.count() > 0 then totalPages = Int((m.items.count() - 1) / 7) + 1 end if if m.mode = "live_streams" then if m.currentCategoryName = invalid then m.currentCategoryName = "Canais" end if m.menuTitle.text = "CANAIS | " + m.currentCategoryName + " (" + (m.page + 1).ToStr() + "/" + totalPages.ToStr() + ")" else if m.mode = "live_categories" then m.menuTitle.text = "CATEGORIAS AO VIVO (" + (m.page + 1).ToStr() + "/" + totalPages.ToStr() + ")" else if m.mode = "favorites" then m.menuTitle.text = "FAVORITOS (" + (m.page + 1).ToStr() + "/" + totalPages.ToStr() + ")" else if m.mode = "account" then m.menuTitle.text = "CONTA" else if m.mode = "movie_categories" then m.menuTitle.text = "CATEGORIAS DE FILMES (" + (m.page + 1).ToStr() + "/" + totalPages.ToStr() + ")" end if for i = 0 to 6 idx = startIndex + i if idx < m.items.count() then item = m.items[idx] txt = itemTitle(item) number = (idx + 1).ToStr() while Len(number) < 3 number = "0" + number end while star = "" if m.mode = "live_streams" or m.mode = "favorites" then if isFavorite(item) then star = " [FAV]" else star = "" end if prefix = " " if i = m.menuIndex then prefix = "> " if m.mode = "home" then labels[i].text = prefix + txt else labels[i].text = prefix + number + " " + txt + star end if end if end for end sub function itemTitle(item as Dynamic) as String if type(item) = "roString" or type(item) = "String" then return item if item = invalid then return "" end if if item.category_name <> invalid then return item.category_name if item.stream_display_name <> invalid then return item.stream_display_name if item.name <> invalid then return item.name if item.title <> invalid then return item.title return "Item" end function function movieCategoryListPageSize() as Integer return 10 end function function currentMovieCategoryListAbsIndex() as Integer return (m.page * movieCategoryListPageSize()) + m.menuIndex end function sub clearMainMenuTexts() if m.menuTitle <> invalid then m.menuTitle.text = "" if m.menu1 <> invalid then m.menu1.text = "" if m.menu2 <> invalid then m.menu2.text = "" if m.menu3 <> invalid then m.menu3.text = "" if m.menu4 <> invalid then m.menu4.text = "" if m.menu5 <> invalid then m.menu5.text = "" if m.menu6 <> invalid then m.menu6.text = "" if m.menu7 <> invalid then m.menu7.text = "" end sub sub returnToMovieCategoriesFromGrid() hideMovieLoading() clearMovieUi() clearMainMenuTexts() setPremiumHomeVisible(false) if m.movieCategories = invalid or m.movieCategories.Count() = 0 then loadMovieCategories() return end if if m.movieCategoryIndex = invalid then m.movieCategoryIndex = 0 if m.movieCategoryIndex < 0 then m.movieCategoryIndex = 0 if m.movieCategoryIndex >= m.movieCategories.Count() then m.movieCategoryIndex = m.movieCategories.Count() - 1 m.mode = "movie_categories" m.items = m.movieCategories m.page = Int(m.movieCategoryIndex / movieCategoryListPageSize()) m.menuIndex = m.movieCategoryIndex - (m.page * movieCategoryListPageSize()) renderMovieCategoriesModern() end sub sub renderMovieCategoriesModern() clearMovieUi() clearMainMenuTexts() m.movieFocusArea = "categories" applyMovieIboCopaLayout() setMovieSidebarVisible(true) totalCats = 0 if m.items <> invalid then totalCats = m.items.Count() if totalCats <= 0 then if m.movieGridTitle <> invalid then m.movieGridTitle.text = "FILMES" m.movieGridTitle.visible = true end if showCatalogHint("Nenhuma categoria de filmes encontrada.") return end if maxPage = Int((totalCats - 1) / movieCategoryListPageSize()) if m.page < 0 then m.page = 0 if m.page > maxPage then m.page = maxPage remain = totalCats - (m.page * movieCategoryListPageSize()) if remain > movieCategoryListPageSize() then remain = movieCategoryListPageSize() if m.menuIndex < 0 then m.menuIndex = 0 if m.menuIndex >= remain then m.menuIndex = remain - 1 if m.menuIndex < 0 then m.menuIndex = 0 m.movieCategoryIndex = currentMovieCategoryListAbsIndex() renderMovieSidebar() if m.movieGridTitle <> invalid then m.movieGridTitle.text = "FILMES • ESCOLHA UMA CATEGORIA" m.movieGridTitle.visible = true end if showCatalogHint("Escolha uma categoria à esquerda e pressione OK ou DIREITA para abrir os filmes.") if m.movieSelectedTitle <> invalid then m.movieSelectedTitle.visible = false m.footer.text = "CIMA/BAIXO = categorias • DIREITA/OK = abrir • VOLTAR = Home" m.top.setFocus(true) end sub sub moveMovieCategoryMenu(delta as Integer) if m.items = invalid or m.items.Count() = 0 then return totalCats = m.items.Count() absIndex = currentMovieCategoryListAbsIndex() + delta if absIndex < 0 then absIndex = totalCats - 1 if absIndex >= totalCats then absIndex = 0 m.page = Int(absIndex / movieCategoryListPageSize()) m.menuIndex = absIndex - (m.page * movieCategoryListPageSize()) renderMovieCategoriesModern() end sub function moviePerPage() as Integer return 8 end function function movieCategoryCountOf(item as Dynamic) as String if item = invalid then return "" if item.category_count <> invalid then return item.category_count.ToStr() if item.count <> invalid then return item.count.ToStr() if item.total <> invalid then return item.total.ToStr() if item.streams_count <> invalid then return item.streams_count.ToStr() return "" end function sub setCatalogPaneVisible(show as Boolean) if m.catalogPaneBg <> invalid then m.catalogPaneBg.visible = show if m.catalogHint <> invalid then if show = false then m.catalogHint.text = "" m.catalogHint.visible = false end if end sub sub showCatalogHint(message as String) setCatalogPaneVisible(true) if m.catalogHint <> invalid then m.catalogHint.text = message m.catalogHint.visible = true end if end sub sub applyPosterCatalogLayout() cardX = [346, 570, 794, 1018, 346, 570, 794, 1018] cardY = [158, 158, 158, 158, 415, 415, 415, 415] for i = 0 to 7 if m.posterFocus[i] <> invalid then m.posterFocus[i].width = 190 m.posterFocus[i].height = 248 m.posterFocus[i].translation = [cardX[i] - 7, cardY[i] - 7] m.posterFocus[i].color = "0xFFD43BFF" end if if m.posters[i] <> invalid then m.posters[i].width = 176 m.posters[i].height = 204 m.posters[i].translation = [cardX[i], cardY[i]] m.posters[i].loadDisplayMode = "scaleToFill" end if if m.posterLabels[i] <> invalid then m.posterLabels[i].width = 184 m.posterLabels[i].height = 40 m.posterLabels[i].translation = [cardX[i] - 4, cardY[i] + 207] m.posterLabels[i].horizAlign = "center" m.posterLabels[i].vertAlign = "center" m.posterLabels[i].wrap = true end if if m.posterProgressBg[i] <> invalid then m.posterProgressBg[i].width = 176 m.posterProgressBg[i].height = 7 m.posterProgressBg[i].translation = [cardX[i], cardY[i] + 197] end if if m.posterProgressFill[i] <> invalid then m.posterProgressFill[i].height = 7 m.posterProgressFill[i].translation = [cardX[i], cardY[i] + 197] end if if m.posterFavBadgeBg[i] <> invalid then m.posterFavBadgeBg[i].translation = [cardX[i] + 6, cardY[i] + 6] end if if m.posterFavBadge[i] <> invalid then m.posterFavBadge[i].translation = [cardX[i] + 6, cardY[i] + 7] end if if m.posterSeenBadgeBg[i] <> invalid then m.posterSeenBadgeBg[i].translation = [cardX[i] + 116, cardY[i] + 6] end if if m.posterSeenBadge[i] <> invalid then m.posterSeenBadge[i].translation = [cardX[i] + 118, cardY[i] + 8] end if end for end sub sub applyMovieIboCopaLayout() setMoviePremiumBaseVisible(true) setCatalogPaneVisible(true) setMoviePreviewVisible(false) setMovieTopBarVisible(false) if m.movieSideShade <> invalid then m.movieSideShade.width = 285 m.movieSideShade.height = 555 m.movieSideShade.translation = [28, 138] m.movieSideShade.color = "0x1B080BE8" m.movieSideShade.visible = true end if if m.movieSideTitle <> invalid then m.movieSideTitle.text = "CATEGORIAS" m.movieSideTitle.width = 250 m.movieSideTitle.translation = [48, 92] m.movieSideTitle.color = "0xFFD43BFF" m.movieSideTitle.visible = true end if catY = [156, 207, 258, 309, 360, 411, 462, 513, 564, 615] for i = 0 to 9 if m.movieCatBg[i] <> invalid then m.movieCatBg[i].width = 255 m.movieCatBg[i].height = 43 m.movieCatBg[i].translation = [42, catY[i]] end if if m.movieCatFocus[i] <> invalid then m.movieCatFocus[i].width = 255 m.movieCatFocus[i].height = 43 m.movieCatFocus[i].translation = [42, catY[i]] end if if m.movieCatLabels[i] <> invalid then m.movieCatLabels[i].width = 190 m.movieCatLabels[i].height = 36 m.movieCatLabels[i].translation = [57, catY[i] + 4] end if if m.movieCatCounts[i] <> invalid then m.movieCatCounts[i].width = 43 m.movieCatCounts[i].height = 36 m.movieCatCounts[i].translation = [245, catY[i] + 4] end if end for if m.movieGridTitle <> invalid then m.movieGridTitle.width = 880 m.movieGridTitle.translation = [340, 92] m.movieGridTitle.color = "0xFFD43BFF" end if applyPosterCatalogLayout() end sub sub applySeriesIboCopaLayout() setMoviePremiumBaseVisible(true) setCatalogPaneVisible(true) setMoviePreviewVisible(false) setMovieTopBarVisible(false) if m.seriesSideShade <> invalid then m.seriesSideShade.width = 285 m.seriesSideShade.height = 555 m.seriesSideShade.translation = [28, 138] m.seriesSideShade.color = "0x1B080BE8" m.seriesSideShade.visible = true end if if m.seriesSideTitle <> invalid then m.seriesSideTitle.text = "CATEGORIAS" m.seriesSideTitle.width = 250 m.seriesSideTitle.translation = [48, 92] m.seriesSideTitle.color = "0xFFD43BFF" m.seriesSideTitle.visible = true end if catY = [156, 207, 258, 309, 360, 411, 462, 513, 564, 615] for i = 0 to 9 if m.seriesCatBg[i] <> invalid then m.seriesCatBg[i].width = 255 m.seriesCatBg[i].height = 43 m.seriesCatBg[i].translation = [42, catY[i]] end if if m.seriesCatFocus[i] <> invalid then m.seriesCatFocus[i].width = 255 m.seriesCatFocus[i].height = 43 m.seriesCatFocus[i].translation = [42, catY[i]] end if if m.seriesCatLabels[i] <> invalid then m.seriesCatLabels[i].width = 190 m.seriesCatLabels[i].height = 36 m.seriesCatLabels[i].translation = [57, catY[i] + 4] end if if m.seriesCatCounts[i] <> invalid then m.seriesCatCounts[i].width = 43 m.seriesCatCounts[i].height = 36 m.seriesCatCounts[i].translation = [245, catY[i] + 4] end if end for if m.movieGridTitle <> invalid then m.movieGridTitle.width = 880 m.movieGridTitle.translation = [340, 92] m.movieGridTitle.color = "0xFFD43BFF" end if applyPosterCatalogLayout() end sub sub setMovieSidebarVisible(show as Boolean) if m.movieSideShade <> invalid then m.movieSideShade.visible = show if m.movieSideTitle <> invalid then m.movieSideTitle.visible = show if m.movieCatBg <> invalid then for each x in m.movieCatBg if x <> invalid then x.visible = false end for end if if m.movieCatFocus <> invalid then for each x in m.movieCatFocus if x <> invalid then x.visible = false end for end if if m.movieCatLabels <> invalid then for each x in m.movieCatLabels if x <> invalid then x.visible = false end for end if if m.movieCatCounts <> invalid then for each x in m.movieCatCounts if x <> invalid then x.visible = false end for end if end sub sub loadSelectedMovieCategoryFromSidebar() if m.movieCategories = invalid or m.movieCategories.Count() = 0 then return if m.movieCategoryIndex < 0 then m.movieCategoryIndex = 0 if m.movieCategoryIndex >= m.movieCategories.Count() then m.movieCategoryIndex = m.movieCategories.Count() - 1 item = m.movieCategories[m.movieCategoryIndex] m.currentMovieCategoryName = itemTitle(item) cid = "" if item.category_id <> invalid then cid = item.category_id.ToStr() if item.id <> invalid and cid = "" then cid = item.id.ToStr() m.movieFocusArea = "grid" if cid = "__movie_continue__" then m.fromMovieSidebar = true showMovieContinueList() m.fromMovieSidebar = false return end if if cid = "__movie_favorites__" then m.fromMovieSidebar = true showMovieFavorites() m.fromMovieSidebar = false return end if if cid = "__search__" then startMovieSearchLoad() return end if if cid = "" then showNotice("ERRO", "Categoria de filmes sem ID.") return end if m.menuIndex = 0 m.page = 0 loadMoviesAuto(cid) end sub sub moveMovieSidebar(delta as Integer) if m.movieCategories = invalid or m.movieCategories.Count() = 0 then return m.movieCategoryIndex = m.movieCategoryIndex + delta if m.movieCategoryIndex < 0 then m.movieCategoryIndex = m.movieCategories.Count() - 1 if m.movieCategoryIndex >= m.movieCategories.Count() then m.movieCategoryIndex = 0 if m.mode = "movie_grid" then renderMovieGrid() else m.page = Int(m.movieCategoryIndex / movieCategoryListPageSize()) m.menuIndex = m.movieCategoryIndex - (m.page * movieCategoryListPageSize()) renderMovieCategoriesModern() end if end sub sub renderMovieSidebar() if m.movieCategories = invalid then return setMovieSidebarVisible(true) totalCats = m.movieCategories.Count() if totalCats <= 0 then return selectedAbs = m.movieCategoryIndex if selectedAbs < 0 then selectedAbs = 0 if selectedAbs >= totalCats then selectedAbs = totalCats - 1 categoryHasFocus = (m.movieFocusArea = "categories") startAt = 0 if selectedAbs > 6 then startAt = selectedAbs - 3 if startAt + 10 > totalCats then startAt = totalCats - 10 if startAt < 0 then startAt = 0 ' Limpa tudo antes para não sobrar CO... ou contador antigo na tela for i = 0 to 9 if m.movieCatBg[i] <> invalid then m.movieCatBg[i].visible = false if m.movieCatFocus[i] <> invalid then m.movieCatFocus[i].visible = false if m.movieCatLabels[i] <> invalid then m.movieCatLabels[i].text = "" m.movieCatLabels[i].visible = false end if if m.movieCatCounts[i] <> invalid then m.movieCatCounts[i].text = "" m.movieCatCounts[i].visible = false end if end for for i = 0 to 9 idx = startAt + i if idx < totalCats then cat = m.movieCategories[idx] name = itemTitle(cat) countTxt = movieCategoryCountOf(cat) cid = "" if cat.category_id <> invalid then cid = cat.category_id.ToStr() if cat.id <> invalid and cid = "" then cid = cat.id.ToStr() special = false if cid = "__movie_continue__" or cid = "__movie_favorites__" or cid = "__search__" then special = true if name = "CONTINUAR ASSISTINDO" then name = "Continuar assistindo" special = true end if if name = "FAVORITOS DE FILMES" then name = "Favoritos" special = true end if if name = "BUSCAR FILMES" then name = "Buscar filmes" special = true end if if special then countTxt = "" ' Só mostra contador se for número puro if countTxt <> "" then isNum = true for j = 1 to Len(countTxt) ch = Mid(countTxt, j, 1) if ch < "0" or ch > "9" then isNum = false exit for end if end for if isNum = false then countTxt = "" end if if m.movieCatBg[i] <> invalid then m.movieCatBg[i].visible = true if idx = selectedAbs and categoryHasFocus = false then m.movieCatBg[i].color = "0x3A0B12E8" else m.movieCatBg[i].color = "0x230A0EE8" end if end if if idx = selectedAbs and m.movieCatFocus[i] <> invalid then m.movieCatFocus[i].visible = true if categoryHasFocus then m.movieCatFocus[i].color = "0xFFD43BFF" else m.movieCatFocus[i].color = "0xFB7185FF" end if if m.movieCatLabels[i] <> invalid then maxText = 21 if countTxt = "" then maxText = 25 m.movieCatLabels[i].text = awCutText(name, maxText) if idx = selectedAbs then m.movieCatLabels[i].color = "0xFFFFFFFF" else m.movieCatLabels[i].color = "0xFFE4E6FF" m.movieCatLabels[i].visible = true end if if m.movieCatCounts[i] <> invalid then m.movieCatCounts[i].text = countTxt if idx = selectedAbs then m.movieCatCounts[i].color = "0xFFFFFFFF" else m.movieCatCounts[i].color = "0xF5B8BDFF" m.movieCatCounts[i].visible = (countTxt <> "") end if end if end for end sub function currentAbsIndexMovie() as Integer return (m.page * moviePerPage()) + m.menuIndex end function function currentMovieItem() as Dynamic idx = currentAbsIndexMovie() if idx >= 0 and idx < m.items.count() then return m.items[idx] return invalid end function function currentAbsIndex() as Integer if m.mode = "live_streams" or m.mode = "favorites" then return currentChannelStreamAbsIndex() end if if m.mode = "series_seasons" or m.mode = "series_episodes" then return currentSeriesPremiumAbsIndex() end if if m.mode = "series_continue_list" then return currentSeriesContinueAbsIndex() end if return (m.page * 7) + m.menuIndex end function function currentItem() as Dynamic idx = currentAbsIndex() if idx >= 0 and idx < m.items.count() then return m.items[idx] return invalid end function sub setMovieTopBarVisible(show as Boolean) ids = ["movieSearchChip", "movieSearchLabel"] orderIds = ["movieOrderChip", "movieOrderLabel"] for each id in orderIds n = m.top.findNode(id) if n <> invalid then n.visible = false end for for each id in ids n = m.top.findNode(id) if n <> invalid then n.visible = show end for end sub sub setMovieGridVisible(show as Boolean) setMovieTopBarVisible(show) setMoviePremiumBaseVisible(show) if show = false then setMoviePreviewVisible(false) if show then hideAllHomeRows() hideAllPosterOverlayBadges() end if if m.movieGridTitle <> invalid then m.movieGridTitle.visible = show if m.movieSelectedTitle <> invalid then m.movieSelectedTitle.visible = show if m.posters <> invalid then for each p in m.posters if p <> invalid then p.visible = show end for end if if m.posterLabels <> invalid then for each l in m.posterLabels if l <> invalid then l.visible = show end for end if if m.posterFocus <> invalid then for each f in m.posterFocus if f <> invalid then f.visible = false end for end if if m.posterSeenBadgeBg <> invalid then for each b in m.posterSeenBadgeBg if b <> invalid then b.visible = false end for end if if m.posterSeenBadge <> invalid then for each b in m.posterSeenBadge if b <> invalid then b.visible = false end for end if if m.posterProgressBg <> invalid then for each b in m.posterProgressBg if b <> invalid then b.visible = false end for end if if m.posterProgressFill <> invalid then for each b in m.posterProgressFill if b <> invalid then b.visible = false end for end if if m.posterFavBadgeBg <> invalid then for each b in m.posterFavBadgeBg if b <> invalid then b.visible = false end for end if if m.posterFavBadge <> invalid then for each b in m.posterFavBadge if b <> invalid then b.visible = false end for end if end sub sub setMovieDetailVisible(show as Boolean) if m.detailBackdrop <> invalid then m.detailBackdrop.visible = show if m.detailDarkLayer <> invalid then m.detailDarkLayer.visible = show if m.detailInfoPanel <> invalid then m.detailInfoPanel.visible = show if m.detailPoster <> invalid then m.detailPoster.visible = show if m.detailTitle <> invalid then m.detailTitle.visible = show if m.detailMeta <> invalid then m.detailMeta.visible = show if m.detailPlot <> invalid then m.detailPlot.visible = false if m.detailButtonBg <> invalid then m.detailButtonBg.visible = show if m.detailFavButtonBg <> invalid then m.detailFavButtonBg.visible = show if m.detailButton <> invalid then m.detailButton.visible = show if m.detailFavButton <> invalid then m.detailFavButton.visible = show setPlotLinesVisible(show) end sub sub clearMovieUi() setPremiumHomeVisible(false) hideActivationText() hideMovieLoading() setMovieSidebarVisible(false) setSeriesSidebarVisible(false) setChannelSidebarVisible(false) hideAllHomeRows() hideAllPosterOverlayBadges() setMovieGridVisible(false) setMovieDetailVisible(false) setPremiumSeriesDetailVisible(false) setMovieTopBarVisible(false) setMoviePremiumBaseVisible(false) setMoviePreviewVisible(false) setMovieLiveSearchVisible(false) setCatalogPaneVisible(false) if m.movieSelectedTitle <> invalid then m.movieSelectedTitle.text = "" m.movieSelectedTitle.visible = false end if clearPlotLines() end sub function awTruncateText(txt as String, maxLen as Integer) as String if txt = invalid then return "" if Len(txt) <= maxLen then return txt if maxLen < 4 then return Left(txt, maxLen) return Left(txt, maxLen - 3) + "..." end function function awWrapText(txt as String, lineLen as Integer, maxLines as Integer) as String if txt = invalid or txt = "" then return "" clean = txt.Replace(Chr(10), " ") clean = clean.Replace(Chr(13), " ") words = clean.Split(" ") lines = [] currentLine = "" truncated = false for wi = 0 to words.Count() - 1 word = words[wi] if word <> "" then candidate = word if currentLine <> "" then candidate = currentLine + " " + word if Len(candidate) <= lineLen then currentLine = candidate else if currentLine <> "" then lines.Push(currentLine) currentLine = word if lines.Count() >= maxLines then truncated = true exit for end if end if end if end for if truncated = false and currentLine <> "" then if lines.Count() < maxLines then lines.Push(currentLine) else truncated = true end if end if if lines.Count() = 0 then return "" if truncated or lines.Count() > maxLines then while lines.Count() > maxLines lines.Pop() end while lastIndex = lines.Count() - 1 lastLine = lines[lastIndex] ' Mantém palavras inteiras antes das reticências para não cortar a sinopse no meio. while Len(lastLine) > lineLen - 3 lastSpace = 0 for ci = Len(lastLine) to 1 step -1 if Mid(lastLine, ci, 1) = " " then lastSpace = ci exit for end if end for if lastSpace > 0 then lastLine = Left(lastLine, lastSpace - 1) else lastLine = Left(lastLine, lineLen - 3) exit while end if end while lines[lastIndex] = lastLine + "..." end if result = "" for li = 0 to lines.Count() - 1 if result <> "" then result = result + Chr(10) result = result + lines[li] end for return result end function sub setPlotLinesVisible(show as Boolean) if m.detailPlot1 <> invalid then m.detailPlot1.visible = show end if if m.detailPlot2 <> invalid then m.detailPlot2.visible = show end if if m.detailPlot3 <> invalid then m.detailPlot3.visible = show end if if m.detailPlot4 <> invalid then m.detailPlot4.visible = show end if if m.detailPlot5 <> invalid then m.detailPlot5.visible = show end if if m.detailPlot6 <> invalid then m.detailPlot6.visible = show end if end sub sub clearPlotLines() if m.detailPlot1 <> invalid then m.detailPlot1.text = "" m.detailPlot1.visible = false end if if m.detailPlot2 <> invalid then m.detailPlot2.text = "" m.detailPlot2.visible = false end if if m.detailPlot3 <> invalid then m.detailPlot3.text = "" m.detailPlot3.visible = false end if if m.detailPlot4 <> invalid then m.detailPlot4.text = "" m.detailPlot4.visible = false end if if m.detailPlot5 <> invalid then m.detailPlot5.text = "" m.detailPlot5.visible = false end if if m.detailPlot6 <> invalid then m.detailPlot6.text = "" m.detailPlot6.visible = false end if end sub function awCutText(txt as String, maxLen as Integer) as String if txt = invalid then return "" end if if Len(txt) <= maxLen then return txt end if return Left(txt, maxLen - 3) + "..." end function function awCleanText(txt as String) as String if txt = invalid then return "" end if s = txt.Replace(Chr(10), " ") s = s.Replace(Chr(13), " ") return s end function function awPart(txt as String, startAt as Integer, amount as Integer) as String s = awCleanText(txt) if startAt > Len(s) then return "" end if return Mid(s, startAt, amount) end function sub setPlotSafe(txt as String) wrapped = awWrapText(txt, 78, 6) lines = wrapped.Split(Chr(10)) plotNodes = [m.detailPlot1, m.detailPlot2, m.detailPlot3, m.detailPlot4, m.detailPlot5, m.detailPlot6] m.detailPlot.text = "" m.detailPlot.visible = false for i = 0 to 5 if plotNodes[i] <> invalid then plotNodes[i].text = "" plotNodes[i].visible = false if i < lines.Count() then plotNodes[i].text = lines[i] plotNodes[i].visible = (lines[i] <> "") end if end if end for end sub function movieCover(item as Dynamic) as String if item = invalid then return "" end if if item.cover <> invalid and item.cover <> "" then return item.cover if item.cover_big <> invalid and item.cover_big <> "" then return item.cover_big if item.movie_image <> invalid and item.movie_image <> "" then return item.movie_image if item.stream_icon <> invalid and item.stream_icon <> "" then return item.stream_icon if item.poster <> invalid and item.poster <> "" then return item.poster return "" end function function movieIdOf(item as Dynamic) as String if item = invalid then return "" end if if item.stream_id <> invalid then return item.stream_id.ToStr() if item.id <> invalid then return item.id.ToStr() if item.movie_id <> invalid then return item.movie_id.ToStr() return "" end function function movieYear(item as Dynamic) as String if item = invalid then return "" end if if item.year <> invalid then return item.year.ToStr() if item.releaseDate <> invalid then return Left(item.releaseDate.ToStr(), 4) if item.releasedate <> invalid then return Left(item.releasedate.ToStr(), 4) if item.release_date <> invalid then return Left(item.release_date.ToStr(), 4) return "" end function function moviePlot(item as Dynamic) as String if item = invalid then return "" end if if item.plot <> invalid and item.plot <> "" then return item.plot if item.description <> invalid and item.description <> "" then return item.description if item.info <> invalid and item.info.plot <> invalid then return item.info.plot return "Sem sinopse disponível." end function function movieMeta(item as Dynamic) as String y = movieYear(item) genre = "" rating = "" duration = "" if item.genre <> invalid then genre = item.genre.ToStr() if item.rating <> invalid then rating = item.rating.ToStr() if item.duration <> invalid then duration = item.duration.ToStr() meta = "" if y <> "" then meta = y if genre <> "" then if meta <> "" then meta = meta + " | " meta = meta + genre end if if duration <> "" then if meta <> "" then meta = meta + " | " meta = meta + duration end if if rating <> "" then if meta <> "" then meta = meta + " | " meta = meta + "Nota " + rating end if return meta end function sub setMoviePremiumBaseVisible(show as Boolean) if m.moviePremiumBackdrop <> invalid then m.moviePremiumBackdrop.visible = show if m.moviePremiumDark <> invalid then m.moviePremiumDark.visible = show if m.moviePremiumAccent <> invalid then m.moviePremiumAccent.visible = show end sub sub setMoviePreviewVisible(show as Boolean) if m.moviePreviewPanel <> invalid then m.moviePreviewPanel.visible = show if m.moviePreviewPoster <> invalid then m.moviePreviewPoster.visible = show if m.moviePreviewTitle <> invalid then m.moviePreviewTitle.visible = show if m.moviePreviewMeta <> invalid then m.moviePreviewMeta.visible = show if m.moviePreviewPlot <> invalid then for each lb in m.moviePreviewPlot if lb <> invalid then lb.visible = show end for end if if m.moviePreviewBadgeBg <> invalid then m.moviePreviewBadgeBg.visible = show if m.moviePreviewBadge <> invalid then m.moviePreviewBadge.visible = show end sub function moviePreviewLines(txt as String) as Object result = ["", "", "", ""] if txt = invalid or txt = "" then return result clean = txt.Replace(Chr(10), " ") clean = clean.Replace(Chr(13), " ") words = clean.Split(" ") lineIndex = 0 currentLine = "" for each word in words if word <> "" then candidate = word if currentLine <> "" then candidate = currentLine + " " + word if Len(candidate) > 23 then result[lineIndex] = currentLine lineIndex = lineIndex + 1 if lineIndex >= 4 then result[3] = awCutText(result[3], 20) + "..." return result end if currentLine = word else currentLine = candidate end if end if end for if lineIndex < 4 then result[lineIndex] = currentLine return result end function sub renderMoviePremiumPreview(item as Dynamic) if item = invalid then setMoviePreviewVisible(false) return end if setMoviePremiumBaseVisible(true) setMoviePreviewVisible(true) previewCover = movieCover(item) if previewCover <> "" then if m.moviePreviewPoster <> invalid then m.moviePreviewPoster.uri = previewCover if m.moviePremiumBackdrop <> invalid then m.moviePremiumBackdrop.uri = previewCover else if m.moviePreviewPoster <> invalid then m.moviePreviewPoster.uri = "" if m.moviePremiumBackdrop <> invalid then m.moviePremiumBackdrop.uri = "pkg:/images/webtv_theme_internal_v251.jpg" end if if m.moviePreviewTitle <> invalid then m.moviePreviewTitle.text = awCutText(itemTitle(item), 42) if m.moviePreviewMeta <> invalid then m.moviePreviewMeta.text = awCutText(movieMeta(item), 25) lines = moviePreviewLines(moviePlot(item)) for i = 0 to 3 if m.moviePreviewPlot[i] <> invalid then m.moviePreviewPlot[i].text = lines[i] end for if m.moviePreviewBadge <> invalid then pct = movieProgressPercent(item) if pct > 0 and pct < 95 then m.moviePreviewBadge.text = "CONTINUAR " + pct.ToStr() + "%" else if isMovieFavorite(item) then m.moviePreviewBadge.text = "★ FAVORITO" else m.moviePreviewBadge.text = "OK = DETALHES" end if end if end sub sub configureKeyboardForLogin() kbBg = m.top.findNode("loginKeyboardBg") if kbBg <> invalid then ' Painel sólido: evita a mistura visual do teclado com os botões da tela de login. kbBg.translation = [70,382] kbBg.width = 1140 kbBg.height = 328 kbBg.color = "0x1A090DFF" end if if m.loginKeyboardTitle <> invalid then m.loginKeyboardTitle.visible = true m.loginKeyboardTitle.translation = [120,392] m.loginKeyboardTitle.width = 1040 end if ' O texto agora aparece diretamente no campo Provedor/Usuário/Senha acima. if m.loginKeyboardValue <> invalid then m.loginKeyboardValue.visible = false if m.loginDynKeyboard <> invalid then m.loginDynKeyboard.translation = [112,438] helpNode = m.top.findNode("loginKeyboardHelp") if helpNode <> invalid then helpNode.visible = true helpNode.text = "O texto aparece no campo acima • OK/Continuar = avançar • Voltar = concluir" helpNode.translation = [120,686] end if end sub sub configureKeyboardForMovieSearch() kbBg = m.top.findNode("loginKeyboardBg") if kbBg <> invalid then kbBg.translation = [70,428] kbBg.width = 1140 kbBg.height = 282 kbBg.color = "0x071426F6" end if if m.loginKeyboardTitle <> invalid then m.loginKeyboardTitle.visible = false if m.loginKeyboardValue <> invalid then m.loginKeyboardValue.visible = false if m.loginDynKeyboard <> invalid then m.loginDynKeyboard.translation = [55,452] m.loginDynKeyboard.purpose = "search" end if helpNode = m.top.findNode("loginKeyboardHelp") if helpNode <> invalid then helpNode.visible = true helpNode.text = "Digite e a busca aparece acima • BUSCAR abre a lista • Opções alterna Filmes/Séries • Voltar retorna" helpNode.translation = [100,686] helpNode.width = 1080 end if end sub sub setMovieLiveSearchVisible(show as Boolean) if m.movieLiveSearchGroup <> invalid then m.movieLiveSearchGroup.visible = show end sub sub clearMovieLiveSearchResults() m.liveSearchItems = [] m.liveSearchLoadedQuery = "" if m.movieLiveResultBg <> invalid then for each n in m.movieLiveResultBg if n <> invalid then n.visible = false end for end if if m.movieLiveResultPoster <> invalid then for each n in m.movieLiveResultPoster if n <> invalid then n.uri = "" n.visible = false end if end for end if if m.movieLiveResultLabel <> invalid then for each n in m.movieLiveResultLabel if n <> invalid then n.text = "" n.visible = false end if end for end if end sub sub renderMovieLiveSearch() setMovieLiveSearchVisible(true) q = m.searchQuery if q = invalid then q = "" isSeriesSearch = (m.searchContentType = "series") contentLabel = "filme" contentLabelPlural = "filmes" screenTitle = "Buscar filmes" typeLabel = "FILMES" typeColor = "0xE31B23FF" if isSeriesSearch then contentLabel = "série" contentLabelPlural = "séries" screenTitle = "Buscar séries" typeLabel = "SÉRIES" typeColor = "0xF43F5EFF" end if if m.movieLiveSearchTitle <> invalid then m.movieLiveSearchTitle.text = screenTitle if m.movieLiveSearchTypeChip <> invalid then m.movieLiveSearchTypeChip.color = typeColor if m.movieLiveSearchTypeLabel <> invalid then m.movieLiveSearchTypeLabel.text = typeLabel if m.movieLiveSearchInput <> invalid then if q = "" then if isSeriesSearch then m.movieLiveSearchInput.text = "Digite o nome da série..." else m.movieLiveSearchInput.text = "Digite o nome do filme..." end if m.movieLiveSearchInput.color = "0x94A3B8FF" else m.movieLiveSearchInput.text = q + " |" m.movieLiveSearchInput.color = "0xFFFFFFFF" end if end if if m.movieLiveSearchHint <> invalid then m.movieLiveSearchHint.text = "OK no BUSCAR = abrir lista completa • Opções = alternar filmes/séries • Voltar = categorias" end if for i = 0 to 4 hasItem = (m.liveSearchItems <> invalid and i < m.liveSearchItems.Count()) if m.movieLiveResultBg[i] <> invalid then m.movieLiveResultBg[i].visible = hasItem if m.movieLiveResultPoster[i] <> invalid then m.movieLiveResultPoster[i].visible = hasItem if hasItem then if isSeriesSearch then m.movieLiveResultPoster[i].uri = seriesCoverOf(m.liveSearchItems[i]) else m.movieLiveResultPoster[i].uri = movieCover(m.liveSearchItems[i]) end if else m.movieLiveResultPoster[i].uri = "" end if end if if m.movieLiveResultLabel[i] <> invalid then m.movieLiveResultLabel[i].visible = hasItem if hasItem then resultTitle = itemTitle(m.liveSearchItems[i]) if isSeriesSearch then resultTitle = seriesTitleOf(m.liveSearchItems[i]) m.movieLiveResultLabel[i].text = awCutText(resultTitle, 24) else m.movieLiveResultLabel[i].text = "" end if end if end for if m.movieLiveSearchStatus <> invalid then if q = "" then m.movieLiveSearchStatus.text = "Comece a digitar: a busca é automática e mostra prévias na hora." else if m.liveSearchLoading then m.movieLiveSearchStatus.text = "Buscando " + contentLabelPlural + ": " + q + "..." else if m.liveSearchLoadedQuery = q.Trim() then if m.liveSearchItems.Count() = 0 then if isSeriesSearch then m.movieLiveSearchStatus.text = "Nenhuma série encontrada para: " + q else m.movieLiveSearchStatus.text = "Nenhum filme encontrado para: " + q end if else m.movieLiveSearchStatus.text = m.liveSearchItems.Count().ToStr() + " resultado(s) rápido(s) para: " + q end if else m.movieLiveSearchStatus.text = "Preparando busca automática..." end if end if end sub sub scheduleMovieLiveSearch() if m.searchContentType <> "movie" and m.searchContentType <> "series" then return q = m.searchQuery if q = invalid then q = "" q = q.Trim() m.liveSearchLoading = false if m.movieSearchDebounceTimer <> invalid then m.movieSearchDebounceTimer.control = "stop" if q = "" then clearMovieLiveSearchResults() renderMovieLiveSearch() return end if m.liveSearchLoadedQuery = "" m.liveSearchItems = [] renderMovieLiveSearch() if m.movieSearchDebounceTimer <> invalid then m.movieSearchDebounceTimer.control = "start" end sub sub onMovieSearchDebounce() if m.mode <> "movie_search_keyboard" then return if m.searchContentType <> "movie" and m.searchContentType <> "series" then return q = m.searchQuery if q = invalid then q = "" q = q.Trim() if q = "" then return m.liveSearchLoading = true m.liveSearchRequestQuery = q renderMovieLiveSearch() safeQ = encodeSearchQuery(q) if m.apiLiveSearchTask <> invalid then endpoint = "/api/roku/vod-search.php" if m.searchContentType = "series" then endpoint = "/api/roku/series-search.php" m.apiLiveSearchTask.control = "STOP" m.apiLiveSearchTask.url = m.apiBase + endpoint + "?token=" + m.token + "&q=" + safeQ m.apiLiveSearchTask.control = "RUN" end if end sub sub onApiLiveSearchResult() if m.apiLiveSearchTask = invalid then return response = m.apiLiveSearchTask.result requestQuery = m.liveSearchRequestQuery currentQuery = m.searchQuery if currentQuery = invalid then currentQuery = "" if requestQuery <> currentQuery.Trim() then return m.liveSearchLoading = false m.liveSearchLoadedQuery = requestQuery m.liveSearchItems = [] if response <> invalid and response.success = true then m.liveSearchItems = normalizeArray(response.data) end if renderMovieLiveSearch() end sub sub openCachedSearchResults() cached = m.liveSearchItems if cached = invalid or cached.Count() = 0 then if m.searchContentType = "series" then performSeriesSearch() else performMovieSearch() return end if setMovieLiveSearchVisible(false) if m.searchContentType = "series" then m.mode = "series_grid_modern" m.items = cached m.seriesGridItems = cached m.currentSeriesCategoryName = "Busca: " + m.searchQuery m.currentSeriesCategoryId = "__search_series__" m.seriesLoadingMore = false m.seriesHasMore = false m.menuIndex = 0 m.page = 0 renderSeriesGridModern() else m.mode = "movie_grid" m.items = cached m.currentMovieCategoryName = "Busca: " + m.searchQuery m.menuIndex = 0 m.page = 0 renderMovieGrid() end if end sub sub openCachedMovieSearchResults() openCachedSearchResults() end sub sub toggleMovieLiveSearchType() if m.searchContentType = "series" then m.searchContentType = "movie" else m.searchContentType = "series" end if if m.movieSearchDebounceTimer <> invalid then m.movieSearchDebounceTimer.control = "stop" m.liveSearchItems = [] m.liveSearchLoadedQuery = "" m.liveSearchRequestQuery = "" m.liveSearchLoading = false renderMovieLiveSearch() q = m.searchQuery if q = invalid then q = "" if q.Trim() <> "" then scheduleMovieLiveSearch() end sub sub startMovieSearchLoad() m.searchContentType = "movie" clearMovieUi() setPremiumHomeVisible(false) hideHomeContinueRow() m.searchQuery = "" m.searchPage = 0 m.searchIndex = 0 m.allMovieItems = [] startSearchDialog("movie") end sub sub startSeriesSearchLoad() clearSeriesModernUi() setPremiumHomeVisible(false) hideHomeContinueRow() m.searchContentType = "series" m.searchQuery = "" m.searchPage = 0 m.searchIndex = 0 startSearchDialog("series") end sub sub startSearchDialog(contentType as String) ' v1.8.223: usa o mesmo teclado ZPLAY aprovado no login para busca de filmes/séries. startSearchZplayKeyboard(contentType) end sub sub startSearchZplayKeyboard(contentType as String) m.searchContentType = contentType if m.searchQuery = invalid then m.searchQuery = "" m.modeBeforeSearchDialog = m.mode m.mode = "movie_search_keyboard" m.searchKeyboardStyle = "zplay" hideKeyboard() clearMovieLiveSearchResults() m.liveSearchLoading = false setMovieLiveSearchVisible(true) if m.loginGroup <> invalid then m.loginGroup.visible = false if m.footer <> invalid then m.footer.visible = false ensureLoginDynKeyboard() configureKeyboardForMovieSearch() renderMovieLiveSearch() m.loginKeyboardField = "search" m.loginKeyboardText = m.searchQuery if m.loginKeyboardGroup <> invalid then m.loginKeyboardGroup.visible = true if m.loginKeyboardValue <> invalid then m.loginKeyboardValue.text = m.searchQuery if m.loginDynKeyboard <> invalid then m.loginDynKeyboard.purpose = "search" m.loginDynKeyboard.mode = "ABC123Lower" m.loginDynKeyboard.text = m.searchQuery m.loginDynKeyboard.visible = true m.loginDynKeyboard.setFocus(true) end if renderLoginKeyboard() end sub sub finishSearchZplayKeyboard(runSearch as Boolean) if m.loginDynKeyboard <> invalid then m.searchQuery = m.loginDynKeyboard.text end if if m.searchQuery = invalid then m.searchQuery = "" if m.movieSearchDebounceTimer <> invalid then m.movieSearchDebounceTimer.control = "stop" destroyLoginDynKeyboardFocus() if m.loginKeyboardGroup <> invalid then m.loginKeyboardGroup.visible = false if m.loginGroup <> invalid then m.loginGroup.visible = false if m.footer <> invalid then m.footer.visible = true setMovieLiveSearchVisible(false) configureKeyboardForLogin() if m.loginDynKeyboard <> invalid then m.loginDynKeyboard.purpose = "login" m.searchKeyboardStyle = "" if runSearch = true and m.searchQuery <> "" then if m.liveSearchLoadedQuery = m.searchQuery.Trim() and m.liveSearchItems <> invalid and m.liveSearchItems.Count() > 0 then openCachedSearchResults() else if m.searchContentType = "series" then performSeriesSearch() else performMovieSearch() end if else if m.searchContentType = "series" then returnToSeriesCategoriesFromGrid() else loadMovieCategories() end if end if end sub sub onSearchDialogButtonSelected() if m.searchInputDialog = invalid then return selected = m.searchInputDialog.buttonSelected if selected = 0 then m.searchDialogAccepted = true txt = m.searchInputDialog.text if txt = invalid then txt = "" m.searchQuery = txt end if m.searchInputDialog.close = true end sub sub onSearchDialogClosed() if m.searchInputDialog <> invalid and m.searchDialogAccepted = true then txt = m.searchInputDialog.text if txt = invalid then txt = "" m.searchQuery = txt if m.searchContentType = "series" then performSeriesSearch() else performMovieSearch() end if else if m.searchContentType = "series" then returnToSeriesCategoriesFromGrid() else loadMovieCategories() end if end if m.searchInputDialog = invalid m.searchDialogAccepted = false end sub sub setupSearchKeyboard() m.searchLetters = [ "A","B","C","D","E","F","G","H","I","J", "K","L","M","N","O","P","Q","R","S","T", "U","V","W","X","Y","Z","0","1","2","3", "4","5","6","7","8","9","ESPAÇO","APAGAR","LIMPAR", "BUSCAR","VOLTAR","" ] m.searchRows = 5 m.searchCols = 10 m.searchIndex = 0 m.mode = "movie_search_keyboard" renderSearchKeyboard() end sub sub setKeyboardVisible(show as Boolean) if m.kbPanel <> invalid then m.kbPanel.visible = show end if if m.kbTitle <> invalid then m.kbTitle.visible = show end if if m.kbInputBg <> invalid then m.kbInputBg.visible = show end if if m.kbInput <> invalid then m.kbInput.visible = show end if if m.kbKeyBg <> invalid then for each bg in m.kbKeyBg if bg <> invalid then bg.visible = show end if end for end if if m.kbKey <> invalid then for each lb in m.kbKey if lb <> invalid then lb.visible = show end if end for end if end sub sub hideKeyboard() setKeyboardVisible(false) end sub sub renderSearchKeyboard() clearHome() setPremiumHomeVisible(false) hideHomeContinueRow() clearMovieUi() m.menuTitle.text = "" m.menu1.text = "" m.menu2.text = "" m.menu3.text = "" m.menu4.text = "" m.menu5.text = "" m.menu6.text = "" m.menu7.text = "" setKeyboardVisible(true) if m.kbTitle <> invalid then if m.searchContentType = "series" then m.kbTitle.text = "BUSCAR SÉRIES" else m.kbTitle.text = "BUSCAR FILMES" end if end if if m.kbInput <> invalid then if m.searchQuery = "" then if m.searchContentType = "series" then m.kbInput.text = "Digite a série: _" else m.kbInput.text = "Digite o filme: _" end if else if m.searchContentType = "series" then m.kbInput.text = "Digite a série: " + m.searchQuery else m.kbInput.text = "Digite o filme: " + m.searchQuery end if end if end if for i = 0 to 41 if i < m.searchLetters.Count() then labelText = m.searchLetters[i] if m.kbKey[i] <> invalid then m.kbKey[i].text = labelText if labelText = "" then m.kbKey[i].visible = false else m.kbKey[i].visible = true end if end if if m.kbKeyBg[i] <> invalid then if labelText = "" then m.kbKeyBg[i].visible = false else m.kbKeyBg[i].visible = true if i = m.searchIndex then m.kbKeyBg[i].color = "0xDC2626FF" else if labelText = "BUSCAR" then m.kbKeyBg[i].color = "0x7F1D1DFF" else if labelText = "APAGAR" or labelText = "LIMPAR" or labelText = "VOLTAR" then m.kbKeyBg[i].color = "0x7F1D1DFF" else if labelText = "ESPAÇO" then m.kbKeyBg[i].color = "0x7F1D1DFF" else m.kbKeyBg[i].color = "0x161012FF" end if end if end if end if end for m.footer.text = "OK = selecionar | Setas = navegar | Voltar = categorias" end sub function searchCurrentAbs() as Integer return m.searchIndex end function sub moveSearchLeft() if m.searchIndex = 0 then return end if m.searchIndex = m.searchIndex - 1 end sub sub moveSearchRight() if m.searchIndex >= 40 then return end if m.searchIndex = m.searchIndex + 1 if m.searchIndex = 41 then m.searchIndex = 40 end if end sub sub moveSearchUp() target = m.searchIndex - 10 if target >= 0 then m.searchIndex = target end if end sub sub moveSearchDown() target = m.searchIndex + 10 if target <= 40 then m.searchIndex = target end if end sub sub applySearchKey() idx = searchCurrentAbs() if idx < 0 or idx >= m.searchLetters.Count() then return end if keyText = m.searchLetters[idx] if keyText = "BUSCAR" then if m.searchContentType = "series" then performSeriesSearch() else performMovieSearch() end if return end if if keyText = "VOLTAR" then hideKeyboard() if m.searchContentType = "series" then returnToSeriesCategoriesFromGrid() else loadMovieCategories() end if return end if if keyText = "APAGAR" then if Len(m.searchQuery) > 0 then m.searchQuery = Left(m.searchQuery, Len(m.searchQuery) - 1) end if renderSearchKeyboard() return end if if keyText = "LIMPAR" then m.searchQuery = "" renderSearchKeyboard() return end if if keyText = "ESPAÇO" then m.searchQuery = m.searchQuery + " " else if keyText <> "" then m.searchQuery = m.searchQuery + keyText end if renderSearchKeyboard() end sub function encodeSearchQuery(q as String) as String if q = invalid then return "" s = q.Trim() ' Normaliza acentos para o painel encontrar melhor tanto maiúsculas como minúsculas. s = s.Replace("Á", "A") s = s.Replace("À", "A") s = s.Replace("Â", "A") s = s.Replace("Ã", "A") s = s.Replace("Ä", "A") s = s.Replace("á", "a") s = s.Replace("à", "a") s = s.Replace("â", "a") s = s.Replace("ã", "a") s = s.Replace("ä", "a") s = s.Replace("É", "E") s = s.Replace("Ê", "E") s = s.Replace("È", "E") s = s.Replace("Ë", "E") s = s.Replace("é", "e") s = s.Replace("ê", "e") s = s.Replace("è", "e") s = s.Replace("ë", "e") s = s.Replace("Í", "I") s = s.Replace("Ì", "I") s = s.Replace("Î", "I") s = s.Replace("Ï", "I") s = s.Replace("í", "i") s = s.Replace("ì", "i") s = s.Replace("î", "i") s = s.Replace("ï", "i") s = s.Replace("Ó", "O") s = s.Replace("Ô", "O") s = s.Replace("Õ", "O") s = s.Replace("Ò", "O") s = s.Replace("Ö", "O") s = s.Replace("ó", "o") s = s.Replace("ô", "o") s = s.Replace("õ", "o") s = s.Replace("ò", "o") s = s.Replace("ö", "o") s = s.Replace("Ú", "U") s = s.Replace("Ù", "U") s = s.Replace("Û", "U") s = s.Replace("Ü", "U") s = s.Replace("ú", "u") s = s.Replace("ù", "u") s = s.Replace("û", "u") s = s.Replace("ü", "u") s = s.Replace("Ç", "C") s = s.Replace("ç", "c") ' URL encode básico para não quebrar busca com espaços, &, +, #, etc. s = s.Replace("%", "%25") s = s.Replace(" ", "%20") s = s.Replace("&", "%26") s = s.Replace("+", "%2B") s = s.Replace("#", "%23") s = s.Replace("?", "%3F") s = s.Replace("/", "%2F") s = s.Replace("=", "%3D") s = s.Replace(Chr(34), "%22") s = s.Replace("'", "%27") return s end function sub performMovieSearch() setMovieLiveSearchVisible(false) q = m.searchQuery if q = "" then showNotice("BUSCAR FILMES", "Digite pelo menos uma letra para buscar.") return end if hideKeyboard() clearHome() clearMovieUi() setPremiumHomeVisible(false) hideHomeContinueRow() m.mode = "loading_movie_search_results" m.menuTitle.text = "BUSCAR FILMES" m.menu1.text = "Buscando: " + q m.menu2.text = "Aguarde..." m.menu3.text = "" m.menu4.text = "" m.menu5.text = "" m.menu6.text = "" m.menu7.text = "" m.footer.text = "Voltar = cancelar" safeQ = encodeSearchQuery(q) url = m.apiBase + "/api/roku/vod-search.php?token=" + m.token + "&q=" + safeQ m.apiGetTask.control = "STOP" m.apiGetTask.control = "STOP" m.apiGetTask.url = url m.apiGetTask.control = "RUN" end sub sub performSeriesSearch() q = m.searchQuery if q = "" then showNotice("BUSCAR SÉRIES", "Digite pelo menos uma letra para buscar.") return end if hideKeyboard() clearHome() clearMovieUi() clearSeriesModernUi() setPremiumHomeVisible(false) hideHomeContinueRow() m.mode = "loading_series_search_results" m.menuTitle.text = "BUSCAR SÉRIES" m.menu1.text = "Buscando: " + q m.menu2.text = "Aguarde..." m.menu3.text = "" m.menu4.text = "" m.menu5.text = "" m.menu6.text = "" m.menu7.text = "" m.footer.text = "Voltar = cancelar" safeQ = encodeSearchQuery(q) url = m.apiBase + "/api/roku/series-search.php?token=" + m.token + "&q=" + safeQ m.apiGetTask.control = "STOP" m.apiGetTask.url = url m.apiGetTask.control = "RUN" end sub ' ========================= ' SÉRIES - AW v1.5.0 ' ========================= function seriesIdOf(item as Dynamic) as String if item = invalid then return "" if item.series_id <> invalid then return item.series_id.ToStr() if item.id <> invalid then return item.id.ToStr() return "" end function function seriesTitleOf(item as Dynamic) as String if item = invalid then return "" if item.name <> invalid then return item.name.ToStr() if item.title <> invalid then return item.title.ToStr() if item.stream_display_name <> invalid then return item.stream_display_name.ToStr() return "" end function function seriesCoverOf(item as Dynamic) as String if item = invalid then return "" if item.cover <> invalid and item.cover <> "" then return item.cover.ToStr() if item.cover_big <> invalid and item.cover_big <> "" then return item.cover_big.ToStr() if item.movie_image <> invalid and item.movie_image <> "" then return item.movie_image.ToStr() if item.stream_icon <> invalid and item.stream_icon <> "" then return item.stream_icon.ToStr() return "" end function function seriesPlotOf(item as Dynamic) as String if item = invalid then return "" if item.plot <> invalid and item.plot <> "" then return item.plot.ToStr() if item.description <> invalid and item.description <> "" then return item.description.ToStr() return "Sem sinopse disponível para esta série." end function function episodeTitleOf(item as Dynamic) as String if item = invalid then return "" if item.title <> invalid then return item.title.ToStr() if item.name <> invalid then return item.name.ToStr() if item.episode_num <> invalid then return "Episódio " + item.episode_num.ToStr() return "Episódio" end function function episodeIdOf(item as Dynamic) as String if item = invalid then return "" if item.id <> invalid then return item.id.ToStr() if item.episode_id <> invalid then return item.episode_id.ToStr() if item.stream_id <> invalid then return item.stream_id.ToStr() return "" end function function seriesPerPage() as Integer return 8 end function function seriesBatchPages() as Integer ' Carrega 5 páginas por vez (5 x 8 = 40 itens) para não pesar o Roku. return 5 end function function seriesBatchLimit() as Integer return seriesPerPage() * seriesBatchPages() end function function seriesListUrl(categoryId as String, pageNum as Integer, limitNum as Integer) as String url = m.apiBase + "/api/roku/series-list.php?token=" + m.token + "&category_id=" + categoryId url = url + "&page=" + pageNum.ToStr() + "&limit=" + limitNum.ToStr() + "&per_page=" + limitNum.ToStr() return url end function sub resetSeriesLazyPaging() m.seriesLazyPage = 1 m.seriesLazyLimit = seriesBatchLimit() m.seriesHasMore = true m.seriesLoadingMore = false m.seriesPendingLazyPage = invalid m.seriesLoadedKeys = {} end sub function seriesUniqueKey(item as Dynamic) as String if item = invalid then return "" sid = seriesIdOf(item) if sid <> "" then return "id_" + sid ttl = seriesTitleOf(item) if ttl <> "" then return "title_" + LCase(ttl) return "" end function function appendUniqueSeriesItems(arr as Object) as Integer if m.items = invalid then m.items = [] if m.seriesLoadedKeys = invalid then m.seriesLoadedKeys = {} added = 0 if arr = invalid then return added for each item in arr key = seriesUniqueKey(item) if key = "" then key = "pos_" + (m.items.Count() + added).ToStr() if m.seriesLoadedKeys.DoesExist(key) = false then m.items.Push(item) m.seriesLoadedKeys[key] = true added = added + 1 end if end for return added end function sub requestMoreSeriesIfNeeded() if m.mode <> "series_grid_modern" then return if m.currentSeriesCategoryId = invalid or m.currentSeriesCategoryId = "" then return if m.seriesHasMore <> true then return if m.seriesLoadingMore = true then return if m.items = invalid then return perPage = seriesPerPage() totalPages = 1 if m.items.Count() > 0 then totalPages = Int((m.items.Count() - 1) / perPage) + 1 ' Quando chegar perto do fim do bloco atual, busca mais 5 páginas. if (m.page + 2) >= totalPages then loadMoreSeriesModern() end if end sub sub loadMoreSeriesModern() if m.currentSeriesCategoryId = invalid or m.currentSeriesCategoryId = "" then return if m.seriesHasMore <> true then return if m.seriesLoadingMore = true then return m.seriesLoadingMore = true nextPage = 2 if m.seriesLazyPage <> invalid then nextPage = m.seriesLazyPage + 1 m.seriesPendingLazyPage = nextPage url = seriesListUrl(m.currentSeriesCategoryId, nextPage, seriesBatchLimit()) m.apiGetTask.control = "STOP" m.apiGetTask.url = url m.apiGetTask.control = "RUN" end sub function seriesCategoryListPageSize() as Integer return 10 end function function currentSeriesCategoryListAbsIndex() as Integer return (m.page * seriesCategoryListPageSize()) + m.menuIndex end function function currentSeriesAbsIndexGrid() as Integer return (m.page * seriesPerPage()) + m.menuIndex end function function seriesPoster(item as Dynamic) as String if item = invalid then return "" if item.cover <> invalid then return item.cover.ToStr() if item.cover_big <> invalid then return item.cover_big.ToStr() if item.movie_image <> invalid then return item.movie_image.ToStr() if item.stream_icon <> invalid then return item.stream_icon.ToStr() if item.logo <> invalid then return item.logo.ToStr() if item.image <> invalid then return item.image.ToStr() return "" end function function seriesCategoryCountOf(item as Dynamic) as String if item = invalid then return "" if item.category_count <> invalid then return item.category_count.ToStr() if item.count <> invalid then return item.count.ToStr() if item.total <> invalid then return item.total.ToStr() if item.series_count <> invalid then return item.series_count.ToStr() return "" end function sub setSeriesSidebarVisible(show as Boolean) if m.seriesSideShade <> invalid then m.seriesSideShade.visible = show if m.seriesSideTitle <> invalid then m.seriesSideTitle.visible = show if m.seriesCatBg <> invalid then for each x in m.seriesCatBg if x <> invalid then x.visible = false end for end if if m.seriesCatFocus <> invalid then for each x in m.seriesCatFocus if x <> invalid then x.visible = false end for end if if m.seriesCatLabels <> invalid then for each x in m.seriesCatLabels if x <> invalid then x.visible = false end for end if if m.seriesCatCounts <> invalid then for each x in m.seriesCatCounts if x <> invalid then x.visible = false end for end if end sub sub hideSeriesSidebarClean() setSeriesSidebarVisible(false) if m.seriesCatBg <> invalid then for each x in m.seriesCatBg if x <> invalid then x.visible = false end for end if if m.seriesCatFocus <> invalid then for each x in m.seriesCatFocus if x <> invalid then x.visible = false end for end if if m.seriesCatLabels <> invalid then for each x in m.seriesCatLabels if x <> invalid then x.text = "" x.visible = false end if end for end if if m.seriesCatCounts <> invalid then for each x in m.seriesCatCounts if x <> invalid then x.text = "" x.visible = false end if end for end if end sub sub renderSeriesSidebar() if m.seriesCategories = invalid then return setSeriesSidebarVisible(true) if m.seriesCatBg <> invalid then for each x in m.seriesCatBg if x <> invalid then x.visible = false end for end if if m.seriesCatFocus <> invalid then for each x in m.seriesCatFocus if x <> invalid then x.visible = false end for end if if m.seriesCatLabels <> invalid then for each x in m.seriesCatLabels if x <> invalid then x.text = "" x.visible = false end if end for end if if m.seriesCatCounts <> invalid then for each x in m.seriesCatCounts if x <> invalid then x.text = "" x.visible = false end if end for end if totalCats = m.seriesCategories.Count() if totalCats <= 0 then return selectedAbs = m.seriesCategoryIndex if selectedAbs < 0 then selectedAbs = 0 if selectedAbs >= totalCats then selectedAbs = totalCats - 1 categoryHasFocus = (m.seriesFocusArea = "categories") startAt = 0 if selectedAbs > 6 then startAt = selectedAbs - 3 if startAt + 10 > totalCats then startAt = totalCats - 10 if startAt < 0 then startAt = 0 for i = 0 to 9 idx = startAt + i if idx < totalCats then cat = m.seriesCategories[idx] name = itemTitle(cat) countTxt = "" if cat.category_count <> invalid then countTxt = cat.category_count.ToStr() if cat.count <> invalid then countTxt = cat.count.ToStr() if cat.total <> invalid then countTxt = cat.total.ToStr() if cat.series_count <> invalid then countTxt = cat.series_count.ToStr() cid = "" if cat.category_id <> invalid then cid = cat.category_id.ToStr() if cat.id <> invalid and cid = "" then cid = cat.id.ToStr() special = false if cid = "__series_continue__" or cid = "__series_favorites__" or cid = "__search_series__" then special = true if name = "CONTINUAR SÉRIES" or name = "CONTINUAR SERIES" then name = "Continuar assistindo" special = true end if if name = "FAVORITOS DE SÉRIES" or name = "FAVORITOS DE SERIES" then name = "Favoritos" special = true end if if name = "BUSCAR SÉRIES" or name = "BUSCAR SERIES" then name = "Buscar séries" special = true end if if special then countTxt = "" if countTxt <> "" then isNum = true for j = 1 to Len(countTxt) ch = Mid(countTxt, j, 1) if ch < "0" or ch > "9" then isNum = false exit for end if end for if isNum = false then countTxt = "" end if if m.seriesCatBg[i] <> invalid then m.seriesCatBg[i].visible = true if idx = selectedAbs and categoryHasFocus = false then m.seriesCatBg[i].color = "0x3A0B12E8" else m.seriesCatBg[i].color = "0x230A0EE8" end if end if if idx = selectedAbs and m.seriesCatFocus[i] <> invalid then m.seriesCatFocus[i].visible = true if categoryHasFocus then m.seriesCatFocus[i].color = "0xFFD43BFF" else m.seriesCatFocus[i].color = "0xFB7185FF" end if if m.seriesCatLabels[i] <> invalid then maxText = 21 if countTxt = "" then maxText = 25 m.seriesCatLabels[i].text = awCutText(name, maxText) if idx = selectedAbs then m.seriesCatLabels[i].color = "0xFFFFFFFF" else m.seriesCatLabels[i].color = "0xFFE4E6FF" m.seriesCatLabels[i].visible = true end if if m.seriesCatCounts[i] <> invalid then m.seriesCatCounts[i].text = countTxt if idx = selectedAbs then m.seriesCatCounts[i].color = "0xFFFFFFFF" else m.seriesCatCounts[i].color = "0xF5B8BDFF" m.seriesCatCounts[i].visible = (countTxt <> "") end if end if end for end sub sub renderSeriesCategoriesModern() clearSeriesModernUi() clearMainMenuTexts() if m.items <> invalid then m.seriesCategories = m.items m.seriesFocusArea = "categories" applySeriesIboCopaLayout() setSeriesSidebarVisible(true) totalCats = 0 if m.items <> invalid then totalCats = m.items.Count() if totalCats <= 0 then if m.movieGridTitle <> invalid then m.movieGridTitle.text = "SÉRIES" m.movieGridTitle.visible = true end if showCatalogHint("Nenhuma categoria de séries encontrada.") return end if maxPage = Int((totalCats - 1) / seriesCategoryListPageSize()) if m.page < 0 then m.page = 0 if m.page > maxPage then m.page = maxPage remain = totalCats - (m.page * seriesCategoryListPageSize()) if remain > seriesCategoryListPageSize() then remain = seriesCategoryListPageSize() if m.menuIndex < 0 then m.menuIndex = 0 if m.menuIndex >= remain then m.menuIndex = remain - 1 if m.menuIndex < 0 then m.menuIndex = 0 m.seriesCategoryIndex = currentSeriesCategoryListAbsIndex() renderSeriesSidebar() if m.movieGridTitle <> invalid then m.movieGridTitle.text = "SÉRIES • ESCOLHA UMA CATEGORIA" m.movieGridTitle.visible = true end if showCatalogHint("Escolha uma categoria à esquerda e pressione OK ou DIREITA para abrir as séries.") if m.movieSelectedTitle <> invalid then m.movieSelectedTitle.visible = false m.footer.text = "CIMA/BAIXO = categorias • DIREITA/OK = abrir • VOLTAR = Home" m.top.setFocus(true) end sub sub moveSeriesCategoryMenu(delta as Integer) if m.items = invalid or m.items.Count() = 0 then return totalCats = m.items.Count() absIndex = currentSeriesCategoryListAbsIndex() + delta if absIndex < 0 then absIndex = totalCats - 1 if absIndex >= totalCats then absIndex = 0 m.page = Int(absIndex / seriesCategoryListPageSize()) m.menuIndex = absIndex - (m.page * seriesCategoryListPageSize()) renderSeriesCategoriesModern() end sub sub returnToSeriesCategoriesFromGrid() hideMovieLoading() clearSeriesModernUi() clearMainMenuTexts() setPremiumHomeVisible(false) if m.seriesCategories = invalid or m.seriesCategories.Count() = 0 then loadSeriesCategories() return end if if m.seriesCategoryIndex = invalid then m.seriesCategoryIndex = 0 if m.seriesCategoryIndex < 0 then m.seriesCategoryIndex = 0 if m.seriesCategoryIndex >= m.seriesCategories.Count() then m.seriesCategoryIndex = m.seriesCategories.Count() - 1 m.mode = "series_categories" m.items = m.seriesCategories m.page = Int(m.seriesCategoryIndex / seriesCategoryListPageSize()) m.menuIndex = m.seriesCategoryIndex - (m.page * seriesCategoryListPageSize()) renderSeriesCategoriesModern() end sub sub clearSeriesModernUi() hideActivationText() hideMovieLoading() hideAllHomeRows() hideHomeBanner() hideAllPosterOverlayBadges() setMovieGridVisible(false) setMovieDetailVisible(false) setPremiumSeriesDetailVisible(false) setMovieSidebarVisible(false) setSeriesSidebarVisible(false) setCatalogPaneVisible(false) end sub function awLoadingLogoUri() as String return "pkg:/images/webtv_loading_frame_1_v251.jpg" end function function awLoadingFrameUri(idx as Integer) as String frame = (idx MOD 4) + 1 return "pkg:/images/webtv_loading_frame_" + frame.ToStr() + "_v251.jpg" end function function awLoadingTitle(defaultTitle as String) as String if m.loadingTitleText <> invalid and m.loadingTitleText <> "" then return m.loadingTitleText if defaultTitle = invalid or defaultTitle = "" then return "Carregando" return defaultTitle end function function awLoadingSub(defaultSub as String) as String if m.loadingSubText <> invalid and m.loadingSubText <> "" then return m.loadingSubText if defaultSub = invalid or defaultSub = "" then return "Aguarde um instante..." return defaultSub end function sub awApplyLoadingBase(title as String, subText as String) if m.movieLoadingGroup <> invalid then m.movieLoadingGroup.visible = true if m.movieLoadingTitle <> invalid then m.movieLoadingTitle.text = awLoadingTitle(title) if m.movieLoadingSub <> invalid then m.movieLoadingSub.text = awLoadingSub(subText) if m.movieLoadingLogo <> invalid then m.movieLoadingLogo.uri = awLoadingLogoUri() m.movieLoadingLogo.width = 142 m.movieLoadingLogo.height = 142 m.movieLoadingLogo.translation = [569, 255] m.movieLoadingLogo.visible = true end if if m.movieLoadingSpinner <> invalid then m.movieLoadingSpinner.visible = false m.movieLoadingStep = 0 updateMovieLoadingSpinner() if m.movieLoadingTimer <> invalid then m.movieLoadingTimer.control = "stop" m.movieLoadingTimer.control = "start" end if m.footer.text = "Aguarde..." end sub sub showSeriesLoading(title as String) clearSeriesModernUi() clearMainMenuTexts() applyAppBackground() awApplyLoadingBase(title, "Buscando séries da categoria...") end sub sub showSeriesInlineLoading() hideMovieLoading() hideAllHomeRows() hideAllPosterOverlayBadges() hideHomeBanner() setPremiumHomeVisible(false) setMovieDetailVisible(false) setPremiumSeriesDetailVisible(false) setMovieGridVisible(true) setMoviePreviewVisible(false) applySeriesIboCopaLayout() setSeriesSidebarVisible(true) ' Mantém as categorias na esquerda e usa apenas a área da direita para o carregamento. m.seriesFocusArea = "categories" renderSeriesSidebar() hideMoviePosterGridForInlineLoad() categoryTitle = "SÉRIES" if m.currentSeriesCategoryName <> invalid and m.currentSeriesCategoryName <> "" then categoryTitle = "SÉRIES • " + UCase(m.currentSeriesCategoryName) end if if m.movieGridTitle <> invalid then m.movieGridTitle.text = categoryTitle m.movieGridTitle.visible = true end if showCatalogHint("Carregando séries da categoria...") if m.movieSelectedTitle <> invalid then m.movieSelectedTitle.visible = false if m.footer <> invalid then m.footer.text = "Aguarde... • VOLTAR = cancelar" m.top.setFocus(true) end sub function currentSeriesGridItem() as Dynamic if m.items = invalid then return invalid idx = currentSeriesAbsIndexGrid() if idx < 0 or idx >= m.items.Count() then return invalid return m.items[idx] end function sub ensureSeriesGridSidebarContext() if m.seriesCategories = invalid or m.seriesCategories.Count() = 0 then return if m.currentSeriesCategoryId <> invalid and m.currentSeriesCategoryId <> "" then for i = 0 to m.seriesCategories.Count() - 1 cat = m.seriesCategories[i] cid = "" if cat.category_id <> invalid then cid = cat.category_id.ToStr() if cid = "" and cat.id <> invalid then cid = cat.id.ToStr() if cid = m.currentSeriesCategoryId then m.seriesCategoryIndex = i return end if end for end if if m.currentSeriesCategoryName <> invalid and m.currentSeriesCategoryName <> "" then wanted = LCase(m.currentSeriesCategoryName) for i = 0 to m.seriesCategories.Count() - 1 if LCase(itemTitle(m.seriesCategories[i])) = wanted then m.seriesCategoryIndex = i return end if end for end if end sub function seriesMeta(item as Dynamic) as String if item = invalid then return "" yearText = "" genreText = "" ratingText = "" episodeText = "" if item.year <> invalid then yearText = item.year.ToStr() if yearText = "" and item.releaseDate <> invalid then yearText = Left(item.releaseDate.ToStr(), 4) if yearText = "" and item.releasedate <> invalid then yearText = Left(item.releasedate.ToStr(), 4) if yearText = "" and item.release_date <> invalid then yearText = Left(item.release_date.ToStr(), 4) if item.genre <> invalid then genreText = item.genre.ToStr() if item.rating <> invalid then ratingText = item.rating.ToStr() if item.episode_run_time <> invalid and item.episode_run_time.ToStr() <> "" then episodeText = item.episode_run_time.ToStr() + " min/ep" meta = "" if yearText <> "" then meta = yearText if genreText <> "" then if meta <> "" then meta = meta + " | " meta = meta + genreText end if if episodeText <> "" then if meta <> "" then meta = meta + " | " meta = meta + episodeText end if if ratingText <> "" then if meta <> "" then meta = meta + " | " meta = meta + "Nota " + ratingText end if return meta end function sub renderSeriesPremiumPreview(item as Dynamic) if item = invalid then setMoviePreviewVisible(false) return end if setMoviePremiumBaseVisible(true) setMoviePreviewVisible(true) previewCover = seriesCoverOf(item) if previewCover <> "" then if m.moviePreviewPoster <> invalid then m.moviePreviewPoster.uri = previewCover if m.moviePremiumBackdrop <> invalid then m.moviePremiumBackdrop.uri = previewCover else if m.moviePreviewPoster <> invalid then m.moviePreviewPoster.uri = "" if m.moviePremiumBackdrop <> invalid then m.moviePremiumBackdrop.uri = "pkg:/images/webtv_theme_internal_v251.jpg" end if if m.moviePreviewTitle <> invalid then m.moviePreviewTitle.text = awCutText(seriesTitleOf(item), 42) if m.moviePreviewMeta <> invalid then m.moviePreviewMeta.text = awCutText(seriesMeta(item), 25) lines = moviePreviewLines(seriesPlotOf(item)) for i = 0 to 3 if m.moviePreviewPlot[i] <> invalid then m.moviePreviewPlot[i].text = lines[i] end for if m.moviePreviewBadge <> invalid then if isSeriesFavorite(item) then m.moviePreviewBadge.text = "★ FAVORITA" else m.moviePreviewBadge.text = "OK = TEMPORADAS" end if end if end sub sub moveSeriesSidebarIntegrated(delta as Integer) if m.seriesCategories = invalid or m.seriesCategories.Count() = 0 then return m.seriesCategoryIndex = m.seriesCategoryIndex + delta if m.seriesCategoryIndex < 0 then m.seriesCategoryIndex = m.seriesCategories.Count() - 1 if m.seriesCategoryIndex >= m.seriesCategories.Count() then m.seriesCategoryIndex = 0 if m.mode = "series_grid_modern" then renderSeriesGridModern() else m.page = Int(m.seriesCategoryIndex / seriesCategoryListPageSize()) m.menuIndex = m.seriesCategoryIndex - (m.page * seriesCategoryListPageSize()) renderSeriesCategoriesModern() end if end sub sub loadSelectedSeriesCategoryFromSidebar() if m.seriesCategories = invalid or m.seriesCategories.Count() = 0 then return if m.seriesCategoryIndex < 0 then m.seriesCategoryIndex = 0 if m.seriesCategoryIndex >= m.seriesCategories.Count() then m.seriesCategoryIndex = m.seriesCategories.Count() - 1 item = m.seriesCategories[m.seriesCategoryIndex] m.currentSeriesCategoryName = itemTitle(item) cid = "" if item.category_id <> invalid then cid = item.category_id.ToStr() if item.id <> invalid and cid = "" then cid = item.id.ToStr() m.seriesFocusArea = "grid" if cid = "__series_continue__" then showSeriesContinueList() return end if if cid = "__series_favorites__" then showSeriesFavorites() return end if if cid = "__search_series__" then startSeriesSearchLoad() return end if if cid = "" then showNotice("ERRO", "Categoria de séries sem ID.") return end if m.currentSeriesCategoryId = cid m.menuIndex = 0 m.page = 0 loadSeriesByCategoryModern(cid) end sub sub renderSeriesGridModern() hideMovieLoading() hideAllPosterOverlayBadges() clearSeriesModernUi() setMovieGridVisible(true) setMovieTopBarVisible(false) setMoviePreviewVisible(false) applySeriesIboCopaLayout() if m.seriesFocusArea <> "categories" and m.seriesFocusArea <> "grid" then m.seriesFocusArea = "grid" if m.seriesFocusArea <> "categories" then ensureSeriesGridSidebarContext() renderSeriesSidebar() if m.catalogHint <> invalid then m.catalogHint.visible = false perPage = seriesPerPage() totalItems = 0 if m.items <> invalid then totalItems = m.items.Count() totalPages = 1 if totalItems > 0 then totalPages = Int((totalItems - 1) / perPage) + 1 if m.currentSeriesCategoryName = invalid then m.currentSeriesCategoryName = "Séries" if m.movieGridTitle <> invalid then m.movieGridTitle.text = "SÉRIES • " + UCase(m.currentSeriesCategoryName) + " • PÁGINA " + (m.page + 1).ToStr() + "/" + totalPages.ToStr() m.movieGridTitle.visible = true end if startIndex = m.page * perPage gridHasFocus = (m.seriesFocusArea = "grid") for i = 0 to perPage - 1 idx = startIndex + i if idx < totalItems then item = m.items[idx] coverUrl = seriesPoster(item) if m.posters[i] <> invalid then m.posters[i].uri = coverUrl m.posters[i].visible = true end if if m.posterLabels[i] <> invalid then m.posterLabels[i].text = awCutText(itemTitle(item), 30) m.posterLabels[i].visible = true end if if m.posterFocus[i] <> invalid then m.posterFocus[i].visible = (gridHasFocus and i = m.menuIndex) if isSeriesFavorite(item) then if m.posterFavBadgeBg[i] <> invalid then m.posterFavBadgeBg[i].visible = true if m.posterFavBadge[i] <> invalid then m.posterFavBadge[i].visible = true else if m.posterFavBadgeBg[i] <> invalid then m.posterFavBadgeBg[i].visible = false if m.posterFavBadge[i] <> invalid then m.posterFavBadge[i].visible = false end if if m.posterProgressBg[i] <> invalid then m.posterProgressBg[i].visible = false if m.posterProgressFill[i] <> invalid then m.posterProgressFill[i].visible = false if m.posterSeenBadgeBg[i] <> invalid then m.posterSeenBadgeBg[i].visible = false if m.posterSeenBadge[i] <> invalid then m.posterSeenBadge[i].visible = false else if m.posters[i] <> invalid then m.posters[i].visible = false if m.posterLabels[i] <> invalid then m.posterLabels[i].visible = false if m.posterFocus[i] <> invalid then m.posterFocus[i].visible = false if m.posterFavBadgeBg[i] <> invalid then m.posterFavBadgeBg[i].visible = false if m.posterFavBadge[i] <> invalid then m.posterFavBadge[i].visible = false if m.posterProgressBg[i] <> invalid then m.posterProgressBg[i].visible = false if m.posterProgressFill[i] <> invalid then m.posterProgressFill[i].visible = false if m.posterSeenBadgeBg[i] <> invalid then m.posterSeenBadgeBg[i].visible = false if m.posterSeenBadge[i] <> invalid then m.posterSeenBadge[i].visible = false end if end for if totalItems = 0 then showCatalogHint("Nenhuma série encontrada nesta categoria.") if m.movieSelectedTitle <> invalid then m.movieSelectedTitle.visible = false if m.seriesFocusArea = "categories" then m.footer.text = "CIMA/BAIXO = categorias • DIREITA/OK = abrir • VOLTAR = Home" else if m.seriesLoadingMore = true then m.footer.text = "OK = temporadas • * = favoritar • Carregando mais séries..." else m.footer.text = "OK = temporadas • * = favoritar • ESQUERDA = categorias • VOLTAR = categorias" end if requestMoreSeriesIfNeeded() m.top.setFocus(true) end sub sub loadSeriesByCategoryModern(categoryId as String) m.mode = "loading_series_inline" m.currentSeriesCategoryId = categoryId resetSeriesLazyPaging() showSeriesInlineLoading() ' Primeira carga: somente 5 páginas de 8 itens. As próximas entram sob demanda. url = seriesListUrl(categoryId, 1, seriesBatchLimit()) m.apiGetTask.control = "STOP" m.apiGetTask.url = url m.apiGetTask.control = "RUN" end sub sub selectSeriesCategoryModern() if m.items = invalid then return idxSel = currentSeriesCategoryListAbsIndex() if idxSel < 0 or idxSel >= m.items.Count() then return item = m.items[idxSel] m.seriesCategoryIndex = idxSel m.currentSeriesCategoryName = itemTitle(item) cid = "" if item.category_id <> invalid then cid = item.category_id.ToStr() if item.id <> invalid and cid = "" then cid = item.id.ToStr() m.seriesFocusArea = "grid" if cid = "__series_continue__" then showSeriesContinueList() return end if if cid = "__series_favorites__" then showSeriesFavorites() return end if if cid = "__search_series__" then startSeriesSearchLoad() return end if if cid = "" then showNotice("ERRO", "Categoria de séries sem ID.") return end if m.currentSeriesCategoryId = cid m.menuIndex = 0 m.page = 0 loadSeriesByCategoryModern(cid) end sub sub moveSeriesGridLeft() if m.menuIndex > 0 then m.menuIndex = m.menuIndex - 1 else if m.page > 0 then m.page = m.page - 1 remain = m.items.Count() - (m.page * seriesPerPage()) if remain > seriesPerPage() then remain = seriesPerPage() m.menuIndex = remain - 1 if m.menuIndex < 0 then m.menuIndex = 0 end if end sub sub moveSeriesGridRight() remain = m.items.Count() - (m.page * seriesPerPage()) if remain > seriesPerPage() then remain = seriesPerPage() if m.menuIndex + 1 < remain then m.menuIndex = m.menuIndex + 1 else if ((m.page + 1) * seriesPerPage()) < m.items.Count() then m.page = m.page + 1 m.menuIndex = 0 end if end sub sub moveSeriesGridUp() if m.menuIndex >= 4 then m.menuIndex = m.menuIndex - 4 else if m.page > 0 then m.page = m.page - 1 remain = m.items.Count() - (m.page * seriesPerPage()) if remain > seriesPerPage() then remain = seriesPerPage() m.menuIndex = remain - 1 if m.menuIndex < 0 then m.menuIndex = 0 end if end sub sub moveSeriesGridDown() idxDown = m.menuIndex + 4 remain = m.items.Count() - (m.page * seriesPerPage()) if remain > seriesPerPage() then remain = seriesPerPage() if idxDown < remain then m.menuIndex = idxDown else if ((m.page + 1) * seriesPerPage()) < m.items.Count() then m.page = m.page + 1 m.menuIndex = 0 end if end sub sub loadSeriesCategories() clearHome() clearMovieUi() setPremiumHomeVisible(false) hideHomeContinueRow() hideHomeFavMoviesRow() m.mode = "loading_series_categories" m.menuTitle.text = "SÉRIES" m.menu1.text = "Carregando categorias de séries..." m.menu2.text = "" m.menu3.text = "" m.menu4.text = "" m.menu5.text = "" m.menu6.text = "" m.menu7.text = "" m.footer.text = "Aguarde..." m.apiGetTask.url = m.apiBase + "/api/roku/series-categories.php?token=" + m.token m.apiGetTask.control = "RUN" end sub sub renderSeriesCategories() clearMovieUi() setPremiumHomeVisible(false) hideHomeContinueRow() hideHomeFavMoviesRow() total = m.items.Count() startAt = m.page * 7 totalPages = 1 if total > 0 then totalPages = Int((total - 1) / 7) + 1 m.menuTitle.text = "CATEGORIAS DE SÉRIES (" + (m.page + 1).ToStr() + "/" + totalPages.ToStr() + ")" labels = [m.menu1, m.menu2, m.menu3, m.menu4, m.menu5, m.menu6, m.menu7] for i = 0 to 6 idx = startAt + i if idx < total then item = m.items[idx] prefix = " " if i = m.menuIndex then prefix = "> " name = "" if item.category_name <> invalid then name = item.category_name.ToStr() else name = seriesTitleOf(item) end if labels[i].text = prefix + awCutText((idx + 1).ToStr() + " " + name, 55) else labels[i].text = "" end if end for m.footer.text = "Cima/Baixo = navegar | OK = abrir categoria | Voltar = Home" end sub sub selectSeriesCategory() idx = (m.page * 7) + m.menuIndex if idx < 0 or idx >= m.items.Count() then return item = m.items[idx] cid = "" if item.category_id <> invalid then cid = item.category_id.ToStr() if cid = "__series_continue__" then showSeriesContinueList() return end if if cid = "__series_favorites__" then showSeriesFavorites() return end if if cid = "__search_series__" then startSeriesSearchLoad() return end if if cid = "" then showNotice("ERRO", "Categoria de séries sem ID.") return end if if item.category_name <> invalid then m.currentSeriesCategoryName = item.category_name.ToStr() else m.currentSeriesCategoryName = "Séries" end if loadSeriesList(cid) end sub sub loadSeriesList(cid as String) clearMovieUi() m.mode = "loading_series_list" m.menuTitle.text = "SÉRIES" m.menu1.text = "Carregando séries..." m.menu2.text = "" m.menu3.text = "" m.menu4.text = "" m.menu5.text = "" m.menu6.text = "" m.menu7.text = "" m.footer.text = "Aguarde..." m.apiGetTask.url = m.apiBase + "/api/roku/series-list.php?token=" + m.token + "&category_id=" + cid m.apiGetTask.control = "RUN" end sub function getSeriesFavorites() as Object sec = CreateObject("roRegistrySection", "awplayer") rawFavs = "" if sec.Exists("favorites_series") then rawFavs = sec.Read("favorites_series") end if if rawFavs = invalid or rawFavs = "" then return [] end if parsedFavs = ParseJson(rawFavs) if parsedFavs = invalid then return [] end if if type(parsedFavs) <> "roArray" then return [] end if ' Segurança: não deixa misturar filme/canal dentro dos favoritos de séries. cleanFavs = [] for each fav in parsedFavs if fav <> invalid then sid = "" if fav.series_id <> invalid then sid = fav.series_id.ToStr() if sid <> "" then cleanFavs.Push(fav) end if end if end for return cleanFavs end function sub saveSeriesFavorites(favs as Object) sec = CreateObject("roRegistrySection", "awplayer") sec.Write("favorites_series", FormatJson(favs)) sec.Flush() end sub function isSeriesFavorite(item as Dynamic) as Boolean if item = invalid then return false sid = seriesIdOf(item) if sid = "" then return false favs = getSeriesFavorites() for each fav in favs favSid = "" if fav.series_id <> invalid then favSid = fav.series_id.ToStr() if favSid = sid then return true end if end for return false end function function cloneSeriesForFavorite(item as Dynamic) as Object fav = {} if item = invalid then return fav fav.series_id = seriesIdOf(item) fav.id = seriesIdOf(item) fav.name = seriesTitleOf(item) fav.title = seriesTitleOf(item) fav.cover = seriesCoverOf(item) fav.cover_big = seriesCoverOf(item) fav.plot = seriesPlotOf(item) fav.genre = "" fav.rating = "" if item.genre <> invalid then fav.genre = item.genre.ToStr() if item.rating <> invalid then fav.rating = item.rating.ToStr() return fav end function sub toggleSeriesFavorite() item = invalid if m.mode = "series_detail" or m.mode = "series_seasons" or m.mode = "series_episodes" then item = m.currentSeries else if m.mode = "series_grid_modern" then item = currentSeriesGridItem() else if m.mode = "series_grid" then item = currentSeriesItem() end if if item = invalid then return sid = seriesIdOf(item) if sid = "" then m.footer.text = "Série sem ID para favoritar" return end if favs = getSeriesFavorites() newFavs = [] removed = false for each fav in favs favSid = "" if fav.series_id <> invalid then favSid = fav.series_id.ToStr() if favSid = sid then removed = true else newFavs.Push(fav) end if end for if removed = false then newFavs.Push(cloneSeriesForFavorite(item)) m.footer.text = "Série adicionada aos favoritos" else m.footer.text = "Série removida dos favoritos" end if saveSeriesFavorites(newFavs) if m.mode = "series_detail" then showSeriesDetail() else if m.mode = "series_grid_modern" then catNameLower = "" if m.currentSeriesCategoryName <> invalid then catNameLower = LCase(m.currentSeriesCategoryName) if Instr(1, catNameLower, "favorit") > 0 then m.items = newFavs m.seriesGridItems = newFavs if m.items.Count() = 0 then hideAllPosterOverlayBadges() showNotice("SÉRIES FAVORITAS", "Nenhuma série favorita ainda. Pressione * em uma série.") return end if if m.menuIndex >= m.items.Count() then m.menuIndex = m.items.Count() - 1 if m.menuIndex < 0 then m.menuIndex = 0 end if hideAllPosterOverlayBadges() renderSeriesGridModern() else if m.mode = "series_grid" then hideAllPosterOverlayBadges() renderSeriesGrid() end if end sub function seriesContinuePerPage() as Integer return 8 end function function currentSeriesContinueAbsIndex() as Integer return (m.page * seriesContinuePerPage()) + m.menuIndex end function function seriesContinueMetaText(ep as Dynamic) as String if ep = invalid then return "Episódio" epTitle = seriesContinueEpisodeNameOf(ep) seasonTxt = "" epNumTxt = "" if ep.season <> invalid then seasonTxt = ep.season.ToStr() if ep.season_number <> invalid and seasonTxt = "" then seasonTxt = ep.season_number.ToStr() if ep.season_num <> invalid and seasonTxt = "" then seasonTxt = ep.season_num.ToStr() if ep.episode_num <> invalid then epNumTxt = ep.episode_num.ToStr() if ep.episode_number <> invalid and epNumTxt = "" then epNumTxt = ep.episode_number.ToStr() if ep.episode <> invalid and epNumTxt = "" then epNumTxt = ep.episode.ToStr() prefix = "" if seasonTxt <> "" and epNumTxt <> "" then prefix = "T" + seasonTxt + " E" + epNumTxt + " - " else if seasonTxt <> "" then prefix = "T" + seasonTxt + " - " end if return prefix + epTitle end function sub showSeriesContinueList() cont = getHomeSeriesContinueList() if cont.Count() = 0 then showNotice("CONTINUAR SÉRIES", "Nenhum episódio em andamento ainda.") return end if clearMovieUi() clearHome() setPremiumHomeVisible(false) m.mode = "series_continue_list" m.items = cont m.page = 0 m.menuIndex = 0 renderSeriesContinueList() end sub sub renderSeriesContinueList() hideMovieLoading() clearSeriesModernUi() clearHome() setPremiumHomeVisible(false) setMovieDetailVisible(false) setPremiumSeriesDetailVisible(false) setMovieGridVisible(true) setMovieTopBarVisible(false) total = m.items.Count() if total <= 0 then showNotice("CONTINUAR SÉRIES", "Nenhum episódio em andamento ainda.") return end if perPage = seriesContinuePerPage() totalPages = Int((total - 1) / perPage) + 1 if m.page < 0 then m.page = 0 if m.page >= totalPages then m.page = totalPages - 1 remain = total - (m.page * perPage) if remain > perPage then remain = perPage if m.menuIndex < 0 then m.menuIndex = 0 if m.menuIndex >= remain then m.menuIndex = remain - 1 if m.menuIndex < 0 then m.menuIndex = 0 if m.seriesCategories <> invalid and m.seriesCategories.Count() > 0 then renderSeriesSidebar() if m.seriesSideTitle <> invalid then m.seriesSideTitle.text = "Séries" m.seriesSideTitle.visible = true end if else setSeriesSidebarVisible(false) end if if m.movieGridTitle <> invalid then m.movieGridTitle.text = "CONTINUAR SÉRIES (" + (m.page + 1).ToStr() + "/" + totalPages.ToStr() + ")" m.movieGridTitle.visible = true end if startIndex = m.page * perPage for i = 0 to perPage - 1 idx = startIndex + i if idx < total then ep = m.items[idx] coverUrl = seriesContinueCoverOf(ep) if coverUrl = "" then coverUrl = "pkg:/images/logo.png" if m.posters[i] <> invalid then m.posters[i].uri = coverUrl m.posters[i].visible = true end if if m.posterLabels[i] <> invalid then name = seriesContinueSeriesNameOf(ep) epName = seriesContinueMetaText(ep) m.posterLabels[i].text = awCutText(name, 17) + Chr(10) + awCutText(epName, 18) m.posterLabels[i].visible = true end if if m.posterFocus[i] <> invalid then if i = m.menuIndex then m.posterFocus[i].visible = true else m.posterFocus[i].visible = false end if pct = episodeProgressPercent(ep) if pct > 0 then if m.posterProgressBg[i] <> invalid then m.posterProgressBg[i].visible = true if m.posterProgressFill[i] <> invalid then w = Int((140 * pct) / 100) if w < 8 then w = 8 if w > 140 then w = 140 m.posterProgressFill[i].width = w m.posterProgressFill[i].visible = true end if else if m.posterProgressBg[i] <> invalid then m.posterProgressBg[i].visible = false if m.posterProgressFill[i] <> invalid then m.posterProgressFill[i].visible = false end if if m.posterFavBadgeBg[i] <> invalid then m.posterFavBadgeBg[i].visible = false if m.posterFavBadge[i] <> invalid then m.posterFavBadge[i].visible = false if m.posterSeenBadgeBg[i] <> invalid then m.posterSeenBadgeBg[i].visible = false if m.posterSeenBadge[i] <> invalid then m.posterSeenBadge[i].visible = false else if m.posters[i] <> invalid then m.posters[i].visible = false if m.posterLabels[i] <> invalid then m.posterLabels[i].visible = false if m.posterFocus[i] <> invalid then m.posterFocus[i].visible = false if m.posterProgressBg[i] <> invalid then m.posterProgressBg[i].visible = false if m.posterProgressFill[i] <> invalid then m.posterProgressFill[i].visible = false if m.posterFavBadgeBg[i] <> invalid then m.posterFavBadgeBg[i].visible = false if m.posterFavBadge[i] <> invalid then m.posterFavBadge[i].visible = false if m.posterSeenBadgeBg[i] <> invalid then m.posterSeenBadgeBg[i].visible = false if m.posterSeenBadge[i] <> invalid then m.posterSeenBadge[i].visible = false end if end for sel = invalid selIdx = currentSeriesContinueAbsIndex() if selIdx >= 0 and selIdx < total then sel = m.items[selIdx] if m.movieSelectedTitle <> invalid then if sel <> invalid then m.movieSelectedTitle.text = awCutText(seriesContinueSeriesNameOf(sel) + " - " + seriesContinueMetaText(sel), 62) else m.movieSelectedTitle.text = "Continuar assistindo" end if m.movieSelectedTitle.visible = true end if m.footer.text = "OK = continuar | * = favoritar | Esquerda/Voltar = categorias" m.top.setFocus(true) end sub sub playSeriesContinueItem() idx = currentSeriesContinueAbsIndex() if idx < 0 or idx >= m.items.Count() then return ep = m.items[idx] m.currentEpisode = ep ' Restaura a série antes de consultar/salvar o progresso do episódio. continueSeries = {} if ep.series_id <> invalid then continueSeries.series_id = ep.series_id continueSeries.id = ep.series_id end if if ep.series_name <> invalid then continueSeries.name = ep.series_name continueSeries.title = ep.series_name end if if ep.series_cover <> invalid then continueSeries.cover = ep.series_cover continueSeries.cover_big = ep.series_cover end if if ep.series_plot <> invalid then continueSeries.plot = ep.series_plot continueSeries.description = ep.series_plot end if if continueSeries.series_id <> invalid or continueSeries.id <> invalid then m.currentSeries = continueSeries title = episodeTitleOf(ep) if title = invalid or title = "" then title = "Episódio" end if url = "" if ep.stream_url <> invalid then url = ep.stream_url if url = "" and ep.url <> invalid then url = ep.url if url = "" and ep.play_url <> invalid then url = ep.play_url if url <> "" then m.returnModeAfterPlayer = "series_continue_list" m.pendingTitle = title finalContEpUrl = awPreferDirectVodUrlForMkv(ep, url, "series") finalContEpFmt = awMediaFormatFromItem(ep, "mp4") if awMaybeShowResumePrompt("series", ep, title, finalContEpUrl, finalContEpFmt, "series_continue_list") then return m.resumeForceStart = false playVideo(title, finalContEpUrl, finalContEpFmt) return end if ' Episódios antigos podem não ter link salvo. Nesse caso abre a série/temporadas. s = m.currentSeries if s = invalid then s = {} sid = "" if s.series_id <> invalid then sid = s.series_id.ToStr() if sid = "" and s.id <> invalid then sid = s.id.ToStr() if sid <> "" then loadSeriesInfo() else showNotice("CONTINUAR SÉRIES", "Link do episódio não salvo. Abra pela temporada.") end if end sub sub showSeriesFavorites() favs = getSeriesFavorites() if favs.Count() = 0 then showNotice("SÉRIES FAVORITAS", "Nenhuma série favorita ainda. Pressione * em uma série.") return end if clearMovieUi() clearSeriesModernUi() clearHome() setPremiumHomeVisible(false) hideHomeContinueRow() hideHomeFavMoviesRow() hideHomeSeriesRows() hideAllPosterOverlayBadges() m.mode = "series_grid_modern" m.seriesFocusArea = "grid" m.items = favs m.seriesGridItems = favs m.currentSeriesCategoryName = "Séries Favoritas" m.page = 0 m.menuIndex = 0 renderSeriesGridModern() end sub function makeHomeSeriesContinueItem(ep as Dynamic) as Object item = {} if ep = invalid then return item item.episode_id = episodeIdOf(ep) item.id = episodeIdOf(ep) item.title = episodeTitleOf(ep) item.name = episodeTitleOf(ep) item.series_id = "" item.series_name = "Série" item.series_cover = "" item.series_plot = "" if m.currentSeries <> invalid then item.series_id = seriesIdOf(m.currentSeries) item.series_name = seriesTitleOf(m.currentSeries) item.series_cover = seriesCoverOf(m.currentSeries) item.series_plot = seriesPlotOf(m.currentSeries) end if if ep.stream_url <> invalid then item.stream_url = ep.stream_url if ep.url <> invalid then item.url = ep.url if ep.play_url <> invalid then item.play_url = ep.play_url if ep.container_extension <> invalid then item.container_extension = ep.container_extension.ToStr() if ep.season <> invalid then item.season = ep.season if ep.season_number <> invalid then item.season_number = ep.season_number if ep.season_num <> invalid then item.season_num = ep.season_num if ep.episode_num <> invalid then item.episode_num = ep.episode_num if ep.episode_number <> invalid then item.episode_number = ep.episode_number if ep.episode <> invalid then item.episode = ep.episode if ep.info <> invalid then item.info = ep.info return item end function function seriesContinueCoverOf(item as Dynamic) as String if item = invalid then return "" if item.series_cover <> invalid and item.series_cover <> "" then return item.series_cover.ToStr() if item.cover <> invalid and item.cover <> "" then return item.cover.ToStr() if item.cover_big <> invalid and item.cover_big <> "" then return item.cover_big.ToStr() if item.movie_image <> invalid and item.movie_image <> "" then return item.movie_image.ToStr() if item.stream_icon <> invalid and item.stream_icon <> "" then return item.stream_icon.ToStr() return "" end function function seriesContinueSeriesNameOf(item as Dynamic) as String if item = invalid then return "Série" if item.series_name <> invalid and item.series_name <> "" then return item.series_name.ToStr() return "Série" end function function seriesContinueEpisodeNameOf(item as Dynamic) as String if item = invalid then return "Episódio" if item.title <> invalid and item.title <> "" then return item.title.ToStr() if item.name <> invalid and item.name <> "" then return item.name.ToStr() return episodeTitleOf(item) end function sub renderSeriesContinuePreview() idx = currentSeriesContinueAbsIndex() if idx < 0 or m.items = invalid or idx >= m.items.Count() then return end if ep = m.items[idx] ' Usa área de detalhe como card limpo, sem lista sobreposta. setMovieDetailVisible(true) cover = seriesContinueCoverOf(ep) if m.detailPoster <> invalid then m.detailPoster.uri = cover m.detailPoster.visible = true end if if m.detailTitle <> invalid then m.detailTitle.text = awCutText(seriesContinueSeriesNameOf(ep), 42) m.detailTitle.visible = true end if epTitle = seriesContinueEpisodeNameOf(ep) pct = episodeProgressPercent(ep) meta = epTitle if pct > 0 and pct < 95 then meta = meta + " | CONT. " + pct.ToStr() + "%" else if pct >= 95 then meta = meta + " | VISTO" end if if m.detailMeta <> invalid then m.detailMeta.text = awCutText(meta, 70) m.detailMeta.visible = true end if plotText = "Clique OK para continuar este episódio." if ep.series_plot <> invalid and ep.series_plot <> "" then plotText = ep.series_plot.ToStr() end if setPlotSafe(plotText) setPlotLinesVisible(true) if m.detailButton <> invalid then m.detailButton.text = "[ OK ] CONTINUAR EPISÓDIO" m.detailButton.visible = true end if end sub function getHomeSeriesContinueList() as Object sec = CreateObject("roRegistrySection", "awplayer") rawList = "" if sec.Exists("home_continue_series") then rawList = sec.Read("home_continue_series") if rawList = invalid or rawList = "" then return [] parsed = ParseJson(rawList) if parsed = invalid then return [] if type(parsed) <> "roArray" then return [] return parsed end function sub saveHomeSeriesContinueList(listData as Object) sec = CreateObject("roRegistrySection", "awplayer") sec.Write("home_continue_series", FormatJson(listData)) sec.Flush() end sub sub addEpisodeToHomeContinue() if m.currentEpisode = invalid then return eid = episodeIdOf(m.currentEpisode) if eid = "" then return oldList = getHomeSeriesContinueList() newList = [] newList.Push(makeHomeSeriesContinueItem(m.currentEpisode)) countAdded = 1 for each oldItem in oldList if episodeIdOf(oldItem) <> eid then if countAdded < 10 then newList.Push(oldItem) countAdded = countAdded + 1 end if end if end for saveHomeSeriesContinueList(newList) end sub sub hideSeriesGridBadges() if m.seriesFavBadgeBg <> invalid then for each b in m.seriesFavBadgeBg if b <> invalid then b.visible = false end if end for end if if m.seriesFavBadge <> invalid then for each b in m.seriesFavBadge if b <> invalid then b.visible = false end if end for end if end sub sub hideAllPosterOverlayBadges() hideSeriesGridBadges() if m.posterFavBadgeBg <> invalid then for each b in m.posterFavBadgeBg if b <> invalid then b.visible = false end for end if if m.posterFavBadge <> invalid then for each b in m.posterFavBadge if b <> invalid then b.visible = false end for end if if m.posterSeenBadgeBg <> invalid then for each b in m.posterSeenBadgeBg if b <> invalid then b.visible = false end for end if if m.posterSeenBadge <> invalid then for each b in m.posterSeenBadge if b <> invalid then b.visible = false end for end if if m.posterProgressBg <> invalid then for each b in m.posterProgressBg if b <> invalid then b.visible = false end for end if if m.posterProgressFill <> invalid then for each b in m.posterProgressFill if b <> invalid then b.visible = false end for end if end sub function homeWindowStart(currentIndex as Integer, totalItems as Integer, visibleCount as Integer, oldStart as Dynamic) as Integer startIndex = 0 if oldStart <> invalid then startIndex = oldStart end if if currentIndex < startIndex then startIndex = currentIndex else if currentIndex >= startIndex + visibleCount then startIndex = currentIndex - visibleCount + 1 end if maxStart = totalItems - visibleCount if maxStart < 0 then maxStart = 0 if startIndex > maxStart then startIndex = maxStart if startIndex < 0 then startIndex = 0 return startIndex end function sub hideAllHomeRows() hideHomeContinueRow() hideHomeFavMoviesRow() hideHomeSeriesRows() end sub sub hideHomeSeriesRows() if m.homeSeriesContinueTitle <> invalid then m.homeSeriesContinueTitle.visible = false if m.homeSeriesFavTitle <> invalid then m.homeSeriesFavTitle.visible = false for i = 0 to 3 if m.homeSerContFocus[i] <> invalid then m.homeSerContFocus[i].visible = false if m.homeSerContPoster[i] <> invalid then m.homeSerContPoster[i].visible = false if m.homeSerContProgBg[i] <> invalid then m.homeSerContProgBg[i].visible = false if m.homeSerContProgFill[i] <> invalid then m.homeSerContProgFill[i].visible = false if m.homeSerContLabel[i] <> invalid then m.homeSerContLabel[i].visible = false if m.homeSerFavFocus[i] <> invalid then m.homeSerFavFocus[i].visible = false if m.homeSerFavPoster[i] <> invalid then m.homeSerFavPoster[i].visible = false if m.homeSerFavStarBg[i] <> invalid then m.homeSerFavStarBg[i].visible = false if m.homeSerFavStar[i] <> invalid then m.homeSerFavStar[i].visible = false if m.homeSerFavLabel[i] <> invalid then m.homeSerFavLabel[i].visible = false end for end sub sub renderHomeSeriesRows() if m.homeSeriesContinueIndex = invalid then m.homeSeriesContinueIndex = 0 end if if m.homeSeriesFavIndex = invalid then m.homeSeriesFavIndex = 0 end if if m.homeSeriesContinueWindowStart = invalid then m.homeSeriesContinueWindowStart = 0 end if if m.homeSeriesFavWindowStart = invalid then m.homeSeriesFavWindowStart = 0 end if cont = getHomeSeriesContinueList() favs = getSeriesFavorites() m.homeSeriesContinueItems = cont m.homeSeriesFavItems = favs if cont.Count() = 0 and favs.Count() = 0 then hideHomeSeriesRows() return end if if cont.Count() > 0 then if m.homeSeriesContinueTitle <> invalid then if cont.Count() > 4 then m.homeSeriesContinueTitle.text = "CONTINUAR SÉRIES (" + cont.Count().ToStr() + ")" else m.homeSeriesContinueTitle.text = "CONTINUAR SÉRIES" end if m.homeSeriesContinueTitle.visible = true end if else if m.homeSeriesContinueTitle <> invalid then m.homeSeriesContinueTitle.visible = false end if if favs.Count() > 0 then if m.homeSeriesFavTitle <> invalid then if favs.Count() > 4 then m.homeSeriesFavTitle.text = "SÉRIES FAVORITAS (" + favs.Count().ToStr() + ")" else m.homeSeriesFavTitle.text = "SÉRIES FAVORITAS" end if m.homeSeriesFavTitle.visible = true end if else if m.homeSeriesFavTitle <> invalid then m.homeSeriesFavTitle.visible = false end if if m.homeSeriesContinueIndex >= cont.Count() then m.homeSeriesContinueIndex = cont.Count() - 1 if m.homeSeriesContinueIndex < 0 then m.homeSeriesContinueIndex = 0 if m.homeSeriesFavIndex >= favs.Count() then m.homeSeriesFavIndex = favs.Count() - 1 if m.homeSeriesFavIndex < 0 then m.homeSeriesFavIndex = 0 visibleCount = 4 contStart = homeWindowStart(m.homeSeriesContinueIndex, cont.Count(), visibleCount, m.homeSeriesContinueWindowStart) favStart = homeWindowStart(m.homeSeriesFavIndex, favs.Count(), visibleCount, m.homeSeriesFavWindowStart) m.homeSeriesContinueWindowStart = contStart m.homeSeriesFavWindowStart = favStart for i = 0 to 3 contIndex = contStart + i if contIndex < cont.Count() then ep = cont[contIndex] if m.homeSerContPoster[i] <> invalid then if ep.series_cover <> invalid then m.homeSerContPoster[i].uri = ep.series_cover m.homeSerContPoster[i].visible = true end if pct = episodeProgressPercent(ep) if pct > 0 then if m.homeSerContProgBg[i] <> invalid then m.homeSerContProgBg[i].visible = true if m.homeSerContProgFill[i] <> invalid then w = Int((96 * pct) / 100) if w < 6 then w = 6 m.homeSerContProgFill[i].width = w m.homeSerContProgFill[i].visible = true end if else if m.homeSerContProgBg[i] <> invalid then m.homeSerContProgBg[i].visible = false if m.homeSerContProgFill[i] <> invalid then m.homeSerContProgFill[i].visible = false end if if m.homeSerContLabel[i] <> invalid then name = "" if ep.series_name <> invalid then name = ep.series_name m.homeSerContLabel[i].text = awCutText(name, 14) m.homeSerContLabel[i].visible = true end if if m.homeSerContFocus[i] <> invalid then if m.homeArea = "seriescontinue" and contIndex = m.homeSeriesContinueIndex then m.homeSerContFocus[i].visible = true else m.homeSerContFocus[i].visible = false end if end if else if m.homeSerContFocus[i] <> invalid then m.homeSerContFocus[i].visible = false if m.homeSerContPoster[i] <> invalid then m.homeSerContPoster[i].visible = false if m.homeSerContProgBg[i] <> invalid then m.homeSerContProgBg[i].visible = false if m.homeSerContProgFill[i] <> invalid then m.homeSerContProgFill[i].visible = false if m.homeSerContLabel[i] <> invalid then m.homeSerContLabel[i].visible = false end if favIndex = favStart + i if favIndex < favs.Count() then ser = favs[favIndex] if m.homeSerFavPoster[i] <> invalid then m.homeSerFavPoster[i].uri = seriesCoverOf(ser) m.homeSerFavPoster[i].visible = true end if if m.homeSerFavStarBg[i] <> invalid then m.homeSerFavStarBg[i].visible = true if m.homeSerFavStar[i] <> invalid then m.homeSerFavStar[i].visible = true if m.homeSerFavLabel[i] <> invalid then m.homeSerFavLabel[i].text = awCutText(seriesTitleOf(ser), 14) m.homeSerFavLabel[i].visible = true end if if m.homeSerFavFocus[i] <> invalid then if m.homeArea = "seriesfav" and favIndex = m.homeSeriesFavIndex then m.homeSerFavFocus[i].visible = true else m.homeSerFavFocus[i].visible = false end if end if else if m.homeSerFavFocus[i] <> invalid then m.homeSerFavFocus[i].visible = false if m.homeSerFavPoster[i] <> invalid then m.homeSerFavPoster[i].visible = false if m.homeSerFavStarBg[i] <> invalid then m.homeSerFavStarBg[i].visible = false if m.homeSerFavStar[i] <> invalid then m.homeSerFavStar[i].visible = false if m.homeSerFavLabel[i] <> invalid then m.homeSerFavLabel[i].visible = false end if end for end sub sub openHomeSeriesContinue() if m.homeSeriesContinueItems = invalid then return if m.homeSeriesContinueItems.Count() = 0 then return if m.homeSeriesContinueIndex = invalid then m.homeSeriesContinueIndex = 0 if m.homeSeriesContinueIndex < 0 then m.homeSeriesContinueIndex = 0 if m.homeSeriesContinueIndex >= m.homeSeriesContinueItems.Count() then m.homeSeriesContinueIndex = 0 ep = m.homeSeriesContinueItems[m.homeSeriesContinueIndex] m.currentEpisode = ep m.returnModeAfterPlayer = "series_episodes" title = episodeTitleOf(ep) if title = "" then title = "Episódio" url = "" if ep.stream_url <> invalid then url = ep.stream_url if url = "" and ep.url <> invalid then url = ep.url if url = "" and ep.play_url <> invalid then url = ep.play_url if url <> "" then m.mode = "series_episodes" finalHomeEpUrl = awPreferDirectVodUrlForMkv(ep, url, "series") finalHomeEpFmt = awMediaFormatFromItem(ep, "mp4") if awMaybeShowResumePrompt("series", ep, title, finalHomeEpUrl, finalHomeEpFmt, "series_episodes") then return m.resumeForceStart = false playVideo(title, finalHomeEpUrl, finalHomeEpFmt) else showNotice("SÉRIE", "Link do episódio não salvo. Abra pela temporada.") end if end sub sub openHomeSeriesFavorite() if m.homeSeriesFavItems = invalid then return if m.homeSeriesFavItems.Count() = 0 then return if m.homeSeriesFavIndex = invalid then m.homeSeriesFavIndex = 0 if m.homeSeriesFavIndex < 0 then m.homeSeriesFavIndex = 0 if m.homeSeriesFavIndex >= m.homeSeriesFavItems.Count() then m.homeSeriesFavIndex = 0 m.currentSeries = m.homeSeriesFavItems[m.homeSeriesFavIndex] showSeriesDetail() end sub sub renderSeriesGrid() if m.seriesCategories <> invalid and m.seriesCategories.Count() > 0 then m.mode = "series_grid_modern" renderSeriesGridModern() return end if hideAllHomeRows() hideAllPosterOverlayBadges() clearHome() setPremiumHomeVisible(false) hideHomeContinueRow() hideHomeFavMoviesRow() setMovieGridVisible(true) total = m.items.Count() startAt = m.page * 6 totalPages = 1 if total > 0 then totalPages = Int((total - 1) / 6) + 1 m.movieGridTitle.text = "SÉRIES | " + awCutText(m.currentSeriesCategoryName, 35) + " (" + (m.page + 1).ToStr() + "/" + totalPages.ToStr() + ")" m.movieGridTitle.visible = true ' Limpa todos os 8 slots antes de desenhar séries antigas, para não sobrar filme/card antigo. for clearIdx = 0 to 7 if m.posters[clearIdx] <> invalid then m.posters[clearIdx].visible = false if m.posterLabels[clearIdx] <> invalid then m.posterLabels[clearIdx].visible = false if m.posterFocus[clearIdx] <> invalid then m.posterFocus[clearIdx].visible = false if m.posterProgressBg[clearIdx] <> invalid then m.posterProgressBg[clearIdx].visible = false if m.posterProgressFill[clearIdx] <> invalid then m.posterProgressFill[clearIdx].visible = false if m.posterSeenBadgeBg[clearIdx] <> invalid then m.posterSeenBadgeBg[clearIdx].visible = false if m.posterSeenBadge[clearIdx] <> invalid then m.posterSeenBadge[clearIdx].visible = false if m.posterFavBadgeBg[clearIdx] <> invalid then m.posterFavBadgeBg[clearIdx].visible = false if m.posterFavBadge[clearIdx] <> invalid then m.posterFavBadge[clearIdx].visible = false end for setMovieTopBarVisible(false) for i = 0 to 5 idx = startAt + i if idx < total then item = m.items[idx] if m.posters[i] <> invalid then m.posters[i].uri = seriesCoverOf(item) m.posters[i].visible = true end if if m.posterLabels[i] <> invalid then m.posterLabels[i].text = awCutText(seriesTitleOf(item), 22) m.posterLabels[i].visible = true end if if isSeriesFavorite(item) then if m.seriesFavBadgeBg[i] <> invalid then m.seriesFavBadgeBg[i].visible = true if m.seriesFavBadge[i] <> invalid then m.seriesFavBadge[i].visible = true else if m.seriesFavBadgeBg[i] <> invalid then m.seriesFavBadgeBg[i].visible = false if m.seriesFavBadge[i] <> invalid then m.seriesFavBadge[i].visible = false end if if m.posterFocus[i] <> invalid then if i = m.menuIndex then m.posterFocus[i].visible = true else m.posterFocus[i].visible = false end if else if m.posters[i] <> invalid then m.posters[i].visible = false if m.posterLabels[i] <> invalid then m.posterLabels[i].visible = false if m.posterFocus[i] <> invalid then m.posterFocus[i].visible = false end if if m.posterProgressBg <> invalid and m.posterProgressBg[i] <> invalid then m.posterProgressBg[i].visible = false if m.posterProgressFill <> invalid and m.posterProgressFill[i] <> invalid then m.posterProgressFill[i].visible = false if m.posterSeenBadgeBg <> invalid and m.posterSeenBadgeBg[i] <> invalid then m.posterSeenBadgeBg[i].visible = false if m.posterSeenBadge <> invalid and m.posterSeenBadge[i] <> invalid then m.posterSeenBadge[i].visible = false if m.posterFavBadgeBg <> invalid and m.posterFavBadgeBg[i] <> invalid then m.posterFavBadgeBg[i].visible = false if m.posterFavBadge <> invalid and m.posterFavBadge[i] <> invalid then m.posterFavBadge[i].visible = false end for m.footer.text = "OK = detalhes | * = favoritar | Esquerda = categorias | Voltar = categorias" end sub function currentSeriesItem() as Dynamic if m.items = invalid then return invalid idx = 0 if m.mode = "series_grid_modern" then idx = currentSeriesAbsIndexGrid() else idx = (m.page * 6) + m.menuIndex end if if idx >= 0 and idx < m.items.Count() then return m.items[idx] return invalid end function sub setPremiumSeriesDetailVisible(show as Boolean) nodes = [m.seriesPremiumBg, m.seriesPremiumDark, m.seriesPremiumLeftShade, m.seriesPremiumLogo, m.seriesPremiumTitle, m.seriesPremiumMeta, m.seriesPremiumPlot1, m.seriesPremiumPlot2, m.seriesPremiumPlot3, m.seriesPremiumPlot4, m.seriesPremiumPlot5, m.seriesPremiumPlot6, m.seriesPremiumContinue, m.seriesPremiumSeasonTitle, m.seriesPremiumEpisodeTitle] for each n in nodes if n <> invalid then n.visible = show end for if m.seriesPremiumBtnBg <> invalid then for each n in m.seriesPremiumBtnBg if n <> invalid then n.visible = show end for end if if m.seriesPremiumBtn <> invalid then for each n in m.seriesPremiumBtn if n <> invalid then n.visible = show end for end if if m.seriesPremiumSeasonBg <> invalid then for each n in m.seriesPremiumSeasonBg if n <> invalid then n.visible = false end for end if if m.seriesPremiumSeasonLabel <> invalid then for each n in m.seriesPremiumSeasonLabel if n <> invalid then n.visible = false end for end if if m.seriesPremiumEpFocus <> invalid then for each n in m.seriesPremiumEpFocus if n <> invalid then n.visible = false end for end if if m.seriesPremiumEpPoster <> invalid then for each n in m.seriesPremiumEpPoster if n <> invalid then n.visible = false end for end if if m.seriesPremiumEpShade <> invalid then for each n in m.seriesPremiumEpShade if n <> invalid then n.visible = false end for end if if m.seriesPremiumEpLabel <> invalid then for each n in m.seriesPremiumEpLabel if n <> invalid then n.visible = false end for end if if m.seriesPremiumEpProgBg <> invalid then for each n in m.seriesPremiumEpProgBg if n <> invalid then n.visible = false end for end if if m.seriesPremiumEpProgFill <> invalid then for each n in m.seriesPremiumEpProgFill if n <> invalid then n.visible = false end for end if end sub function seriesBgOf(item as Dynamic) as String if item = invalid then return "" if item.backdrop_path <> invalid and item.backdrop_path <> "" then return item.backdrop_path.ToStr() if item.backdrop <> invalid and item.backdrop <> "" then return item.backdrop.ToStr() if item.background <> invalid and item.background <> "" then return item.background.ToStr() if item.cover_big <> invalid and item.cover_big <> "" then return item.cover_big.ToStr() if item.cover <> invalid and item.cover <> "" then return item.cover.ToStr() if item.stream_icon <> invalid and item.stream_icon <> "" then return item.stream_icon.ToStr() return "" end function sub renderSeriesPremiumBase() if m.currentSeries = invalid then return setPremiumSeriesDetailVisible(true) bg = seriesBgOf(m.currentSeries) if bg = "" then bg = seriesCoverOf(m.currentSeries) if m.seriesPremiumBg <> invalid then if bg <> "" then m.seriesPremiumBg.uri = bg else m.seriesPremiumBg.uri = "pkg:/images/webtv_theme_internal_v251.jpg" end if if m.seriesPremiumTitle <> invalid then m.seriesPremiumTitle.text = awCutText(seriesTitleOf(m.currentSeries), 32) meta = seriesMeta(m.currentSeries) if meta = "" then meta = "Série" if m.seriesPremiumMeta <> invalid then m.seriesPremiumMeta.text = awCutText(meta, 65) plot = seriesPlotOf(m.currentSeries) ' Usa linhas menores para caber na largura real dos Labels da Roku. ' Assim nenhuma linha ganha reticências no meio; somente a última linha ' recebe "..." quando a sinopse ultrapassa o espaço disponível. wrapped = awWrapText(plot, 48, 6) lines = wrapped.Split(Chr(10)) plotNodes = [m.seriesPremiumPlot1, m.seriesPremiumPlot2, m.seriesPremiumPlot3, m.seriesPremiumPlot4, m.seriesPremiumPlot5, m.seriesPremiumPlot6] for i = 0 to 5 if plotNodes[i] <> invalid then plotNodes[i].text = "" if i < lines.Count() then plotNodes[i].text = lines[i] end if end for if m.seriesPremiumBtn <> invalid then if isSeriesFavorite(m.currentSeries) then m.seriesPremiumBtn[2].text = "Remover" else m.seriesPremiumBtn[2].text = "Favoritos" end if end sub sub renderSeriesPremiumButtons(focusIndex as Integer) if m.seriesPremiumBtnBg = invalid then return for i = 0 to 2 if m.seriesPremiumBtnBg[i] <> invalid then if m.seriesPremiumFocusArea = "buttons" and i = focusIndex then m.seriesPremiumBtnBg[i].color = "0xE11D48FF" else m.seriesPremiumBtnBg[i].color = "0x161012DD" end if end for end sub function seriesPremiumPerPage() as Integer return 4 end function function currentSeriesPremiumAbsIndex() as Integer return (m.page * seriesPremiumPerPage()) + m.menuIndex end function sub renderSeriesPremiumSeasons() if m.seriesSeasons = invalid then return total = m.seriesSeasons.Count() startAt = m.page * seriesPremiumPerPage() for i = 0 to seriesPremiumPerPage() - 1 idx = startAt + i if idx < total then if m.seriesPremiumSeasonBg[i] <> invalid then m.seriesPremiumSeasonBg[i].visible = true if m.seriesPremiumFocusArea = "seasons" and i = m.menuIndex then m.seriesPremiumSeasonBg[i].color = "0xE11D48FF" else m.seriesPremiumSeasonBg[i].color = "0x161012DD" end if if m.seriesPremiumSeasonLabel[i] <> invalid then m.seriesPremiumSeasonLabel[i].text = awCutText(m.seriesSeasons[idx].name, 18) m.seriesPremiumSeasonLabel[i].visible = true end if end if end for end sub function episodeImageOf(ep as Dynamic) as String if ep = invalid then return "" if ep.info <> invalid and ep.info.movie_image <> invalid and ep.info.movie_image <> "" then return ep.info.movie_image.ToStr() if ep.movie_image <> invalid and ep.movie_image <> "" then return ep.movie_image.ToStr() if ep.cover <> invalid and ep.cover <> "" then return ep.cover.ToStr() if ep.stream_icon <> invalid and ep.stream_icon <> "" then return ep.stream_icon.ToStr() return seriesCoverOf(m.currentSeries) end function sub renderSeriesPremiumEpisodes() if m.items = invalid then return total = m.items.Count() startAt = m.page * seriesPremiumPerPage() for i = 0 to seriesPremiumPerPage() - 1 idx = startAt + i if idx < total then ep = m.items[idx] if m.seriesPremiumEpFocus[i] <> invalid then m.seriesPremiumEpFocus[i].visible = (m.seriesPremiumFocusArea = "episodes" and i = m.menuIndex) if m.seriesPremiumEpPoster[i] <> invalid then img = episodeImageOf(ep) if img <> "" then m.seriesPremiumEpPoster[i].uri = img m.seriesPremiumEpPoster[i].visible = true end if if m.seriesPremiumEpShade[i] <> invalid then m.seriesPremiumEpShade[i].visible = true if m.seriesPremiumEpLabel[i] <> invalid then if ep.episode_num <> invalid then epName = "Episódio " + ep.episode_num.ToStr() else epName = "Episódio " + (idx + 1).ToStr() m.seriesPremiumEpLabel[i].text = awCutText(epName, 25) m.seriesPremiumEpLabel[i].visible = true end if pct = episodeProgressPercent(ep) if m.seriesPremiumEpProgBg[i] <> invalid then m.seriesPremiumEpProgBg[i].visible = true if m.seriesPremiumEpProgFill[i] <> invalid then m.seriesPremiumEpProgFill[i].width = Int(218 * pct / 100) if pct > 0 then m.seriesPremiumEpProgFill[i].visible = true else m.seriesPremiumEpProgFill[i].visible = false end if end if end for end sub sub showSeriesDetail() comingMode = m.mode if comingMode = "series_grid_modern" then m.seriesCameFromModernGrid = true m.seriesGridItems = m.items m.seriesGridPage = m.page m.seriesGridIndex = m.menuIndex else if comingMode = "series_grid" then m.seriesCameFromModernGrid = false m.seriesGridItems = m.items m.seriesGridPage = m.page m.seriesGridIndex = m.menuIndex end if item = invalid if comingMode = "series_grid_modern" and m.currentSeries <> invalid then item = m.currentSeries else item = currentSeriesItem() if m.currentSeries <> invalid and comingMode <> "series_grid_modern" and comingMode <> "series_grid" then item = m.currentSeries end if if item = invalid then return m.currentSeries = item m.mode = "series_detail" m.seriesPremiumFocusArea = "buttons" m.menuIndex = 0 clearMovieUi() clearHome() hideSeriesSidebarClean() setPremiumHomeVisible(false) hideHomeContinueRow() hideHomeFavMoviesRow() setPlotLinesVisible(false) renderSeriesPremiumBase() renderSeriesPremiumButtons(0) if m.seriesPremiumSeasonTitle <> invalid then m.seriesPremiumSeasonTitle.visible = false if m.seriesPremiumEpisodeTitle <> invalid then m.seriesPremiumEpisodeTitle.visible = false if m.seriesPremiumContinue <> invalid then m.seriesPremiumContinue.text = "OK = carregar temporadas | * = favoritos | Voltar = séries" m.footer.text = "OK = carregar temporadas | Direita/Esquerda = botões | Voltar = séries" end sub sub restoreSeriesGridAfterPremium() clearMovieUi() clearHome() setPremiumSeriesDetailVisible(false) setPremiumHomeVisible(false) hideHomeContinueRow() hideHomeFavMoviesRow() if m.seriesCameFromModernGrid = true then if m.seriesGridItems <> invalid then m.items = m.seriesGridItems end if if m.items = invalid or m.items.Count() = 0 then loadSeriesCategories() return end if m.mode = "series_grid_modern" m.seriesFocusArea = "grid" if m.seriesGridPage <> invalid then m.page = m.seriesGridPage else m.page = 0 if m.seriesGridIndex <> invalid then m.menuIndex = m.seriesGridIndex else m.menuIndex = 0 hideAllPosterOverlayBadges() renderSeriesGridModern() else if m.seriesGridItems <> invalid then m.items = m.seriesGridItems end if m.mode = "series_grid" if m.seriesGridPage <> invalid then m.page = m.seriesGridPage else m.page = 0 if m.seriesGridIndex <> invalid then m.menuIndex = m.seriesGridIndex else m.menuIndex = 0 renderSeriesGrid() end if end sub sub loadSeriesInfo() if m.currentSeries = invalid then return sid = seriesIdOf(m.currentSeries) if sid = "" then showNotice("ERRO", "Série sem ID.") return end if m.mode = "loading_series_info" m.menuTitle.text = "SÉRIES" m.menu1.text = "Carregando temporadas..." m.menu2.text = "" m.menu3.text = "" m.menu4.text = "" m.menu5.text = "" m.menu6.text = "" m.menu7.text = "" m.footer.text = "Aguarde..." m.apiGetTask.url = m.apiBase + "/api/roku/series-info.php?token=" + m.token + "&series_id=" + sid m.apiGetTask.control = "RUN" end sub function awSeasonNumberValue(v as Dynamic) as Integer if v = invalid then return 9999 txt = v.ToStr() num = 0 found = false for i = 1 to Len(txt) ch = Mid(txt, i, 1) d = -1 if ch = "0" then d = 0 if ch = "1" then d = 1 if ch = "2" then d = 2 if ch = "3" then d = 3 if ch = "4" then d = 4 if ch = "5" then d = 5 if ch = "6" then d = 6 if ch = "7" then d = 7 if ch = "8" then d = 8 if ch = "9" then d = 9 if d >= 0 then num = (num * 10) + d found = true end if end for if found = false then return 9999 return num end function function awSeasonDisplayName(v as Dynamic) as String n = awSeasonNumberValue(v) if n = 9999 then if v = invalid then return "Temporada" return v.ToStr() end if return "Temporada " + n.ToStr() end function sub sortSeriesSeasonsAscending() if m.seriesSeasons = invalid then return total = m.seriesSeasons.Count() if total <= 1 then return for i = 0 to total - 2 for j = i + 1 to total - 1 a = awSeasonNumberValue(m.seriesSeasons[i].season) b = awSeasonNumberValue(m.seriesSeasons[j].season) if a > b then tmp = m.seriesSeasons[i] m.seriesSeasons[i] = m.seriesSeasons[j] m.seriesSeasons[j] = tmp end if end for end for end sub sub prepareSeriesSeasons(data as Dynamic) m.currentSeriesInfo = data m.seriesSeasons = [] m.seriesEpisodesBySeason = {} episodes = invalid if data.episodes <> invalid then episodes = data.episodes if episodes = invalid and data.data <> invalid and data.data.episodes <> invalid then episodes = data.data.episodes if episodes = invalid then showNotice("SÉRIE", "Nenhum episódio retornou para esta série.") return end if if type(episodes) = "roAssociativeArray" then for each seasonKey in episodes seasonEpisodes = episodes[seasonKey] if type(seasonEpisodes) = "roArray" and seasonEpisodes.Count() > 0 then seasonObj = { season: seasonKey.ToStr(), name: awSeasonDisplayName(seasonKey) } m.seriesSeasons.Push(seasonObj) m.seriesEpisodesBySeason[seasonKey.ToStr()] = seasonEpisodes end if end for else if type(episodes) = "roArray" then seasonObj = { season: "1", name: "Temporada 1" } m.seriesSeasons.Push(seasonObj) m.seriesEpisodesBySeason["1"] = episodes end if sortSeriesSeasonsAscending() if m.seriesSeasons.Count() = 0 then showNotice("SÉRIE", "Nenhuma temporada encontrada.") return end if m.mode = "series_seasons" m.items = m.seriesSeasons m.page = 0 m.menuIndex = 0 renderSeriesSeasons() end sub sub renderSeriesSeasons() clearMovieUi() clearHome() setPremiumHomeVisible(false) hideHomeContinueRow() hideHomeFavMoviesRow() m.mode = "series_seasons" m.seriesPremiumFocusArea = "seasons" renderSeriesPremiumBase() renderSeriesPremiumButtons(-1) renderSeriesPremiumSeasons() if m.seriesPremiumEpisodeTitle <> invalid then m.seriesPremiumEpisodeTitle.visible = false if m.seriesPremiumContinue <> invalid then m.seriesPremiumContinue.text = "Escolha uma temporada" m.footer.text = "Esquerda/Direita = temporadas | OK = episódios | Voltar = detalhes" end sub sub selectSeriesSeason() idx = currentSeriesPremiumAbsIndex() if idx < 0 or idx >= m.items.Count() then return season = m.items[idx] m.currentSeason = season.season eps = m.seriesEpisodesBySeason[m.currentSeason] if eps = invalid or eps.Count() = 0 then showNotice("TEMPORADA", "Nenhum episódio nesta temporada.") return end if m.mode = "series_episodes" m.items = eps m.page = 0 m.menuIndex = 0 renderSeriesEpisodes() end sub sub renderSeriesEpisodes() clearMovieUi() clearHome() setPremiumHomeVisible(false) hideHomeContinueRow() hideHomeFavMoviesRow() m.mode = "series_episodes" m.seriesPremiumFocusArea = "episodes" renderSeriesPremiumBase() renderSeriesPremiumButtons(-1) if m.seriesPremiumSeasonTitle <> invalid then m.seriesPremiumSeasonTitle.visible = true renderSeriesPremiumSeasons() if m.seriesPremiumEpisodeTitle <> invalid then m.seriesPremiumEpisodeTitle.text = "Episódios - Temporada " + m.currentSeason m.seriesPremiumEpisodeTitle.visible = true end if if m.seriesPremiumContinue <> invalid then idx = currentSeriesPremiumAbsIndex() total = 0 if m.items <> invalid then total = m.items.Count() m.seriesPremiumContinue.text = "Episódio " + (idx + 1).ToStr() + " de " + total.ToStr() end if renderSeriesPremiumEpisodes() m.footer.text = "Esquerda/Direita = episódios | OK = assistir | Cima = temporadas | Voltar = temporadas" end sub function episodeProgressKey(ep as Dynamic) as String if ep = invalid then return "" end if epKeyId = episodeIdOf(ep) if epKeyId = "" then return "" end if ' Itens da aba Continuar carregam o series_id. Usar esse ID primeiro ' evita salvar/ler o progresso usando a série aberta anteriormente. seriesKeyId = "" if ep.series_id <> invalid and ep.series_id <> "" then seriesKeyId = ep.series_id.ToStr() if seriesKeyId = "" and ep.parent_id <> invalid and ep.parent_id <> "" then seriesKeyId = ep.parent_id.ToStr() if seriesKeyId = "" and m.currentSeries <> invalid then seriesKeyId = seriesIdOf(m.currentSeries) end if if seriesKeyId = "" then seriesKeyId = "series" end if return "series_ep_pos_" + seriesKeyId + "_" + epKeyId end function function episodeProgressRaw(ep as Dynamic) as String epKey = episodeProgressKey(ep) if epKey = "" then return "" end if sec = CreateObject("roRegistrySection", "awplayer") if sec.Exists(epKey) then return sec.Read(epKey) end if return "" end function function episodeProgressPosition(ep as Dynamic) as Integer rawProgress = episodeProgressRaw(ep) if rawProgress = "" then return 0 end if parts = rawProgress.Split("|") if parts.Count() > 0 then valPos = Val(parts[0]) if valPos > 0 then return Int(valPos) end if end if return 0 end function function episodeProgressDuration(ep as Dynamic) as Integer rawProgress = episodeProgressRaw(ep) if rawProgress = "" then return 0 end if parts = rawProgress.Split("|") if parts.Count() > 1 then valDur = Val(parts[1]) if valDur > 0 then return Int(valDur) end if end if return 0 end function function episodeProgressPercent(ep as Dynamic) as Integer epPos = episodeProgressPosition(ep) epDur = episodeProgressDuration(ep) if epPos <= 0 or epDur <= 0 then return 0 end if epPct = Int((epPos * 100) / epDur) if epPct < 0 then epPct = 0 if epPct > 100 then epPct = 100 return epPct end function function episodeResumeSeconds(ep as Dynamic) as Integer epPct = episodeProgressPercent(ep) if epPct > 0 and epPct < 95 then return episodeProgressPosition(ep) end if return 0 end function sub saveCurrentEpisodeProgress() if m.currentEpisode = invalid then return end if epKey = episodeProgressKey(m.currentEpisode) if epKey = "" then return end if epPos = 0 epDur = 0 progressInfo = awSafeVodProgress() if progressInfo.valid = true then epPos = progressInfo.position epDur = progressInfo.duration end if if epPos <= 0 or epDur <= 0 then return if epPos > epDur then epPos = epDur end if sec = CreateObject("roRegistrySection", "awplayer") sec.Write(epKey, epPos.ToStr() + "|" + epDur.ToStr()) sec.Flush() addEpisodeToHomeContinue() end sub sub onProgressSaveTimer() if m.mode <> "player" then return if m.video = invalid then return stateText = "" if m.video.state <> invalid then stateText = m.video.state.ToStr() if stateText <> "playing" and stateText <> "paused" then return if m.returnModeAfterPlayer = "movie_detail" then saveCurrentMovieProgress() else if m.returnModeAfterPlayer = "series_episodes" or m.returnModeAfterPlayer = "series_continue_list" then saveCurrentEpisodeProgress() end if end sub sub playEpisode() if m.mode = "series_episodes" then idx = currentSeriesPremiumAbsIndex() else idx = currentAbsIndex() end if if idx < 0 or idx >= m.items.Count() then return ep = m.items[idx] m.currentEpisode = ep m.returnModeAfterPlayer = "series_episodes" title = episodeTitleOf(ep) if title = invalid or title = "" then title = "Episódio" end if m.pendingTitle = title streamUrl = "" if ep.stream_url <> invalid and ep.stream_url <> "" then streamUrl = ep.stream_url.ToStr() if streamUrl = "" and ep.url <> invalid and ep.url <> "" then streamUrl = ep.url.ToStr() if streamUrl = "" and ep.play_url <> invalid and ep.play_url <> "" then streamUrl = ep.play_url.ToStr() if streamUrl <> "" then m.returnModeAfterPlayer = "series_episodes" finalEpUrl = awPreferDirectVodUrlForMkv(ep, streamUrl, "series") finalEpFmt = awMediaFormatFromItem(ep, "mp4") if awMaybeShowResumePrompt("series", ep, title, finalEpUrl, finalEpFmt, "series_episodes") then return m.resumeForceStart = false playVideo(title, finalEpUrl, finalEpFmt) return end if epId = episodeIdOf(ep) if epId = "" then showNotice("ERRO", "Episódio sem ID.") return end if if awMaybeShowResumePrompt("series", ep, title, "", "", "series_episodes") then return m.resumeForceStart = false m.mode = "loading_episode_play" m.menuTitle.text = "EPISÓDIO" m.menu1.text = "Abrindo episódio..." m.footer.text = "Aguarde..." m.apiGetTask.url = m.apiBase + "/api/roku/series-play.php?token=" + m.token + "&episode_id=" + epId m.apiGetTask.control = "RUN" end sub sub loadMovieCategories() setPremiumHomeVisible(false) hideHomeBanner() clearMovieUi() m.mode = "loading_movie_categories" setPremiumHomeVisible(false) hideListEpg() m.menuTitle.text = "FILMES" m.menu1.text = "Carregando categorias de filmes..." m.menu2.text = "" m.menu3.text = "" m.menu4.text = "" m.menu5.text = "" m.menu6.text = "" m.menu7.text = "" m.footer.text = "Aguarde..." url = m.apiBase + "/api/roku/vod-categories.php?token=" + m.token m.apiGetTask.url = url m.apiGetTask.control = "RUN" end sub sub onMovieLoadingTimer() if m.movieLoadingGroup = invalid then return if m.movieLoadingGroup.visible <> true then return m.movieLoadingStep = m.movieLoadingStep + 1 updateMovieLoadingSpinner() end sub sub showMovieLoading(title as String) clearMovieUi() clearMainMenuTexts() hideAllHomeRows() hideHomeBanner() applyAppBackground() awApplyLoadingBase(title, "Buscando filmes da categoria...") end sub sub updateMovieLoadingSpinner() idx = m.movieLoadingStep MOD 4 if m.movieLoadingLogo <> invalid then m.movieLoadingLogo.uri = awLoadingFrameUri(idx) if idx = 0 then m.movieLoadingLogo.width = 136 m.movieLoadingLogo.height = 136 m.movieLoadingLogo.translation = [572, 258] else if idx = 1 then m.movieLoadingLogo.width = 146 m.movieLoadingLogo.height = 146 m.movieLoadingLogo.translation = [567, 253] else if idx = 2 then m.movieLoadingLogo.width = 152 m.movieLoadingLogo.height = 152 m.movieLoadingLogo.translation = [564, 250] else m.movieLoadingLogo.width = 146 m.movieLoadingLogo.height = 146 m.movieLoadingLogo.translation = [567, 253] end if m.movieLoadingLogo.visible = true end if if m.movieLoadingTitle <> invalid then baseTitle = awLoadingTitle("Carregando") dots = "" if idx = 1 then dots = "." if idx = 2 then dots = ".." if idx = 3 then dots = "..." m.movieLoadingTitle.text = baseTitle + dots end if if m.movieLoadingSpinner <> invalid then m.movieLoadingSpinner.visible = false end sub sub hideMovieLoading() if m.movieLoadingGroup <> invalid then m.movieLoadingGroup.visible = false if m.movieLoadingTimer <> invalid then m.movieLoadingTimer.control = "stop" end if end sub sub hideMoviePosterGridForInlineLoad() if m.posters <> invalid then for each node in m.posters if node <> invalid then node.visible = false end for end if if m.posterLabels <> invalid then for each node in m.posterLabels if node <> invalid then node.visible = false end for end if if m.posterFocus <> invalid then for each node in m.posterFocus if node <> invalid then node.visible = false end for end if if m.posterProgressBg <> invalid then for each node in m.posterProgressBg if node <> invalid then node.visible = false end for end if if m.posterProgressFill <> invalid then for each node in m.posterProgressFill if node <> invalid then node.visible = false end for end if if m.posterSeenBadgeBg <> invalid then for each node in m.posterSeenBadgeBg if node <> invalid then node.visible = false end for end if if m.posterSeenBadge <> invalid then for each node in m.posterSeenBadge if node <> invalid then node.visible = false end for end if if m.posterFavBadgeBg <> invalid then for each node in m.posterFavBadgeBg if node <> invalid then node.visible = false end for end if if m.posterFavBadge <> invalid then for each node in m.posterFavBadge if node <> invalid then node.visible = false end for end if end sub sub showMovieInlineLoading() hideMovieLoading() hideAllHomeRows() hideAllPosterOverlayBadges() hideHomeBanner() setPremiumHomeVisible(false) setMovieDetailVisible(false) setPremiumSeriesDetailVisible(false) setMovieGridVisible(true) setMoviePreviewVisible(false) applyMovieIboCopaLayout() setMovieSidebarVisible(true) ' Mantém as categorias na esquerda e usa apenas a área da direita para o carregamento. m.movieFocusArea = "categories" renderMovieSidebar() hideMoviePosterGridForInlineLoad() categoryTitle = "FILMES" if m.currentMovieCategoryName <> invalid and m.currentMovieCategoryName <> "" then categoryTitle = "FILMES • " + UCase(m.currentMovieCategoryName) end if if m.movieGridTitle <> invalid then m.movieGridTitle.text = categoryTitle m.movieGridTitle.visible = true end if showCatalogHint("Carregando filmes da categoria...") if m.movieSelectedTitle <> invalid then m.movieSelectedTitle.visible = false if m.footer <> invalid then m.footer.text = "Aguarde... • VOLTAR = cancelar" m.top.setFocus(true) end sub sub loadMoviesAuto(categoryId as String) m.currentMovieCategoryId = categoryId m.mode = "loading_movies_inline" showMovieInlineLoading() url = m.apiBase + "/api/roku/vod-movies.php?token=" + m.token + "&category_id=" + categoryId m.apiMovieTask.control = "STOP" m.apiMovieTask.url = url m.apiMovieTask.control = "RUN" end sub sub onApiMovieResult() ' Ignora retorno atrasado depois que o usuário cancelou ou saiu da área. if m.mode <> "loading_movies_inline" and m.mode <> "loading_movies_auto" then return hideMovieLoading() res = m.apiMovieTask.result if res = invalid or res.success <> true then msg = "Falha ao carregar filmes" if res <> invalid and res.message <> invalid then msg = res.message if isInvalidProviderMessage(msg) then logoutToLogin(msg) else ' Mantém a tela integrada aberta e mostra o erro somente no painel da direita. m.mode = "movie_grid" m.movieFocusArea = "categories" m.items = [] m.menuIndex = 0 m.page = 0 renderMovieGrid() showCatalogHint(msg) if m.footer <> invalid then m.footer.text = "CIMA/BAIXO = categorias • DIREITA/OK = tentar novamente • VOLTAR = Home" end if return end if data = res.data arr = normalizeArray(data) if arr.count() = 0 then m.mode = "movie_grid" m.movieFocusArea = "categories" m.items = [] m.menuIndex = 0 m.page = 0 renderMovieGrid() showCatalogHint("Nenhum filme encontrado nesta categoria.") return end if m.mode = "movie_grid" m.movieFocusArea = "grid" m.items = arr m.menuIndex = 0 m.page = 0 renderMovieGrid() end sub sub loadMovies(categoryId as String) ' Compatibilidade com chamadas antigas: usa o carregamento integrado na mesma tela. loadMoviesAuto(categoryId) end sub function movieProgressKey(item as Dynamic) as String if item = invalid then return "" end if movieKeyId = movieIdOf(item) if movieKeyId = "" then return "" end if return "vod_progress_" + movieKeyId end function function movieProgressRaw(item as Dynamic) as String progressKey = movieProgressKey(item) if progressKey = "" then return "" end if sec = CreateObject("roRegistrySection", "awplayer") if sec.Exists(progressKey) then return sec.Read(progressKey) end if return "" end function function movieProgressPosition(item as Dynamic) as Integer rawProgress = movieProgressRaw(item) if rawProgress = "" then return 0 end if progressParts = rawProgress.Split("|") if progressParts.Count() > 0 then progressValue = Val(progressParts[0]) if progressValue > 0 then return Int(progressValue) end if end if return 0 end function function movieProgressDuration(item as Dynamic) as Integer rawProgress = movieProgressRaw(item) if rawProgress = "" then return 0 end if progressParts = rawProgress.Split("|") if progressParts.Count() > 1 then durationValue = Val(progressParts[1]) if durationValue > 0 then return Int(durationValue) end if end if return 0 end function function movieProgressPercent(item as Dynamic) as Integer progressPos = movieProgressPosition(item) progressDur = movieProgressDuration(item) if progressPos <= 0 or progressDur <= 0 then return 0 end if progressPct = Int((progressPos * 100) / progressDur) if progressPct < 0 then progressPct = 0 if progressPct > 100 then progressPct = 100 return progressPct end function function movieResumeSeconds(item as Dynamic) as Integer resumePct = movieProgressPercent(item) if resumePct > 0 and resumePct < 95 then return movieProgressPosition(item) end if return 0 end function sub saveCurrentMovieProgress() if m.currentMovie = invalid then return end if saveKey = movieProgressKey(m.currentMovie) if saveKey = "" then return end if savePos = 0 saveDur = 0 progressInfo = awSafeVodProgress() if progressInfo.valid = true then savePos = progressInfo.position saveDur = progressInfo.duration end if if savePos <= 0 or saveDur <= 0 then return if savePos > saveDur then savePos = saveDur end if sec = CreateObject("roRegistrySection", "awplayer") sec.Write(saveKey, savePos.ToStr() + "|" + saveDur.ToStr()) sec.Flush() addMovieToHomeContinue() end sub ' PATCH v1.8.90 FILMES: grade moderna sempre com categorias laterais e retorno limpo sub renderMovieGrid() hideMovieLoading() hideAllHomeRows() hideAllPosterOverlayBadges() m.top.setFocus(true) clearHome() setMovieDetailVisible(false) setPremiumSeriesDetailVisible(false) setMovieGridVisible(true) setMoviePreviewVisible(false) applyMovieIboCopaLayout() if m.movieFocusArea <> "categories" and m.movieFocusArea <> "grid" then m.movieFocusArea = "grid" renderMovieSidebar() if m.catalogHint <> invalid then m.catalogHint.visible = false perPage = moviePerPage() totalPages = 1 if m.items <> invalid and m.items.Count() > 0 then totalPages = Int((m.items.Count() - 1) / perPage) + 1 if m.currentMovieCategoryName = invalid then m.currentMovieCategoryName = "Filmes" if m.movieGridTitle <> invalid then m.movieGridTitle.text = "FILMES • " + UCase(m.currentMovieCategoryName) + " • PÁGINA " + (m.page + 1).ToStr() + "/" + totalPages.ToStr() m.movieGridTitle.visible = true end if totalItems = 0 if m.items <> invalid then totalItems = m.items.Count() startIndex = m.page * perPage gridHasFocus = (m.movieFocusArea = "grid") for i = 0 to perPage - 1 idx = startIndex + i if idx < totalItems then item = m.items[idx] coverUrl = movieCover(item) if coverUrl <> "" then m.posters[i].uri = coverUrl else m.posters[i].uri = "" m.posters[i].visible = true m.posterLabels[i].text = awCutText(itemTitle(item), 30) m.posterLabels[i].visible = true if m.posterFocus[i] <> invalid then m.posterFocus[i].visible = (gridHasFocus and i = m.menuIndex) if isMovieFavorite(item) then if m.posterFavBadgeBg[i] <> invalid then m.posterFavBadgeBg[i].visible = true if m.posterFavBadge[i] <> invalid then m.posterFavBadge[i].text = "★" m.posterFavBadge[i].visible = true end if else if m.posterFavBadgeBg[i] <> invalid then m.posterFavBadgeBg[i].visible = false if m.posterFavBadge[i] <> invalid then m.posterFavBadge[i].visible = false end if progressPct = movieProgressPercent(item) if progressPct > 0 then if m.posterProgressBg[i] <> invalid then m.posterProgressBg[i].visible = true if m.posterProgressFill[i] <> invalid then fillWidth = Int((176 * progressPct) / 100) if fillWidth < 6 then fillWidth = 6 m.posterProgressFill[i].width = fillWidth m.posterProgressFill[i].visible = true end if if m.posterSeenBadgeBg[i] <> invalid then m.posterSeenBadgeBg[i].visible = true if m.posterSeenBadge[i] <> invalid then if progressPct >= 85 then m.posterSeenBadge[i].text = "VISTO" else m.posterSeenBadge[i].text = "CONT." m.posterSeenBadge[i].visible = true end if else if m.posterProgressBg[i] <> invalid then m.posterProgressBg[i].visible = false if m.posterProgressFill[i] <> invalid then m.posterProgressFill[i].visible = false if m.posterSeenBadgeBg[i] <> invalid then m.posterSeenBadgeBg[i].visible = false if m.posterSeenBadge[i] <> invalid then m.posterSeenBadge[i].visible = false end if else if m.posters[i] <> invalid then m.posters[i].visible = false if m.posterLabels[i] <> invalid then m.posterLabels[i].visible = false if m.posterFocus[i] <> invalid then m.posterFocus[i].visible = false if m.posterProgressBg[i] <> invalid then m.posterProgressBg[i].visible = false if m.posterProgressFill[i] <> invalid then m.posterProgressFill[i].visible = false if m.posterSeenBadgeBg[i] <> invalid then m.posterSeenBadgeBg[i].visible = false if m.posterSeenBadge[i] <> invalid then m.posterSeenBadge[i].visible = false if m.posterFavBadgeBg[i] <> invalid then m.posterFavBadgeBg[i].visible = false if m.posterFavBadge[i] <> invalid then m.posterFavBadge[i].visible = false end if end for if totalItems = 0 then showCatalogHint("Nenhum filme encontrado nesta categoria.") if m.movieSelectedTitle <> invalid then m.movieSelectedTitle.visible = false if m.movieFocusArea = "categories" then m.footer.text = "CIMA/BAIXO = categorias • DIREITA/OK = abrir • VOLTAR = Home" else m.footer.text = "OK = detalhes • * = favoritar • ESQUERDA = categorias • VOLTAR = categorias" end if end sub function getContinueItems() as Object return getHomeContinueList() end function sub showMovieContinueList() if m.fromMovieSidebar <> true then m.movieCategoryIndex = 0 hideActivationText() contItems = getHomeContinueList() if contItems.Count() = 0 then showNotice("CONTINUAR ASSISTINDO", "Nenhum filme em andamento ainda.") return end if clearMovieUi() clearHome() setPremiumHomeVisible(false) m.mode = "movie_grid" m.movieFocusArea = "grid" if m.movieCategories = invalid or m.movieCategories.Count() = 0 then m.movieCategories = [{category_name:"CONTINUAR ASSISTINDO", category_id:"__movie_continue__"}] m.items = contItems m.currentMovieCategoryName = "Continuar Assistindo" m.menuIndex = 0 m.page = 0 renderMovieGrid() end sub function getMovieFavorites() as Object sec = CreateObject("roRegistrySection", "awplayer") rawFavs = "" if sec.Exists("favorites_movies") then rawFavs = sec.Read("favorites_movies") end if if rawFavs = invalid or rawFavs = "" then return [] end if parsedFavs = ParseJson(rawFavs) if parsedFavs = invalid then return [] end if if type(parsedFavs) <> "roArray" then return [] end if return parsedFavs end function sub saveMovieFavorites(favs as Object) sec = CreateObject("roRegistrySection", "awplayer") sec.Write("favorites_movies", FormatJson(favs)) sec.Flush() end sub function isMovieFavorite(item as Dynamic) as Boolean if item = invalid then return false end if sidFav = movieIdOf(item) if sidFav = "" then return false end if favs = getMovieFavorites() for each fav in favs if movieIdOf(fav) = sidFav then return true end if end for return false end function function cloneMovieForFavorite(item as Dynamic) as Object favItem = {} if item = invalid then return favItem end if favItem.stream_id = movieIdOf(item) favItem.id = movieIdOf(item) favItem.name = itemTitle(item) favItem.stream_display_name = itemTitle(item) favItem.cover = movieCover(item) favItem.cover_big = movieCover(item) favItem.plot = moviePlot(item) favItem.genre = "" favItem.rating = "" favItem.duration = "" favItem.releasedate = "" favItem.container_extension = "mp4" if item.genre <> invalid then favItem.genre = item.genre.ToStr() if item.rating <> invalid then favItem.rating = item.rating.ToStr() if item.duration <> invalid then favItem.duration = item.duration.ToStr() if item.releasedate <> invalid then favItem.releasedate = item.releasedate.ToStr() if item.container_extension <> invalid then favItem.container_extension = item.container_extension.ToStr() if item.stream_url <> invalid then favItem.stream_url = item.stream_url if item.url <> invalid then favItem.url = item.url if item.play_url <> invalid then favItem.play_url = item.play_url return favItem end function sub toggleMovieFavorite() item = invalid if m.mode = "movie_detail" then item = m.currentMovie else if m.mode = "movie_grid" then item = currentMovieItem() end if if item = invalid then return end if movieSid = movieIdOf(item) if movieSid = "" then m.footer.text = "Filme sem ID para favoritar" return end if favs = getMovieFavorites() newFavs = [] removed = false for each fav in favs if movieIdOf(fav) = movieSid then removed = true else newFavs.Push(fav) end if end for if removed = false then newFavs.Push(cloneMovieForFavorite(item)) m.footer.text = "Filme adicionado aos favoritos" else m.footer.text = "Filme removido dos favoritos" end if saveMovieFavorites(newFavs) if m.mode = "movie_detail" then showMovieDetail() else if m.mode = "movie_grid" then if m.currentMovieCategoryName = "Favoritos de Filmes" then m.items = newFavs if m.items.Count() = 0 then showNotice("FAVORITOS DE FILMES", "Nenhum filme favorito ainda.") return end if if currentAbsIndexMovie() >= m.items.Count() then m.page = 0 m.menuIndex = 0 end if end if renderMovieGrid() end if end sub sub showMovieFavorites() if m.fromMovieSidebar <> true then m.movieCategoryIndex = 2 favs = getMovieFavorites() if favs.Count() = 0 then showNotice("FAVORITOS DE FILMES", "Nenhum filme favorito ainda. Entre em Filmes e pressione * no filme.") return end if clearMovieUi() setPremiumHomeVisible(false) hideHomeContinueRow() m.mode = "movie_grid" m.movieFocusArea = "grid" if m.movieCategories = invalid or m.movieCategories.Count() = 0 then m.movieCategories = [{category_name:"FAVORITOS DE FILMES", category_id:"__movie_favorites__"}] m.items = favs m.currentMovieCategoryName = "Favoritos de Filmes" m.menuIndex = 0 m.page = 0 renderMovieGrid() end sub function firstRealMovieCategoryIndex() as Integer if m.movieCategories = invalid then return 0 for i = 0 to m.movieCategories.Count() - 1 cat = m.movieCategories[i] cid = "" if cat.category_id <> invalid then cid = cat.category_id.ToStr() if cat.id <> invalid and cid = "" then cid = cat.id.ToStr() ' Ignora atalhos especiais: continuar, buscar e favoritos if cid <> "__movie_continue__" and cid <> "__search__" and cid <> "__movie_favorites__" and cid <> "" then return i end if end for return 0 end function sub openFirstMovieCategoryDirect() if m.movieCategories = invalid or m.movieCategories.Count() = 0 then showNotice("FILMES", "Nenhuma categoria de filme retornou da API.") return end if idx = firstRealMovieCategoryIndex() if idx < 0 then idx = 0 if idx >= m.movieCategories.Count() then idx = 0 item = m.movieCategories[idx] m.movieCategoryIndex = idx m.currentMovieCategoryName = itemTitle(item) cid = "" if item.category_id <> invalid then cid = item.category_id.ToStr() if item.id <> invalid and cid = "" then cid = item.id.ToStr() if cid = "__movie_continue__" then showMovieContinueList() return end if if cid = "__movie_favorites__" then showMovieFavorites() return end if if cid = "__search__" then startMovieSearchLoad() return end if if cid = "" then showNotice("ERRO", "Categoria de filmes sem ID.") return end if loadMoviesAuto(cid) end sub sub selectMovieCategory() if m.mode = "movie_categories" then idxSel = currentMovieCategoryListAbsIndex() if m.items = invalid or idxSel < 0 or idxSel >= m.items.Count() then return item = m.items[idxSel] m.movieCategoryIndex = idxSel else item = currentItem() if item = invalid then return m.movieCategoryIndex = currentAbsIndex() end if if item = invalid then return m.currentMovieCategoryName = itemTitle(item) cid = "" if item.category_id <> invalid then cid = item.category_id.ToStr() if item.id <> invalid then cid = item.id.ToStr() m.movieFocusArea = "grid" if cid = "__movie_continue__" then showMovieContinueList() return end if if cid = "__search__" then startMovieSearchLoad() return end if if cid = "__movie_favorites__" then showMovieFavorites() return end if if cid = "" then showNotice("ERRO", "Categoria de filmes sem ID.") return end if loadMovies(cid) end sub sub showMovieDetail() item = currentMovieItem() if m.currentMovie <> invalid and m.returnModeAfterPlayer = "movie_detail" then item = m.currentMovie if item = invalid then return m.mode = "movie_detail" m.currentMovie = item clearHome() setMovieGridVisible(false) setMovieSidebarVisible(false) if m.movieSelectedTitle <> invalid then m.movieSelectedTitle.text = "" m.movieSelectedTitle.visible = false end if setMovieDetailVisible(true) coverDetail = movieCover(item) if coverDetail <> "" then m.detailPoster.uri = coverDetail if m.detailBackdrop <> invalid then m.detailBackdrop.uri = coverDetail else m.detailPoster.uri = "" if m.detailBackdrop <> invalid then m.detailBackdrop.uri = "pkg:/images/webtv_theme_internal_v251.jpg" end if m.detailTitle.text = awCutText(itemTitle(item), 42) detailMetaText = movieMeta(item) moviePct = movieProgressPercent(item) if moviePct > 0 then detailMetaText = detailMetaText + " | Assistido " + moviePct.ToStr() + "%" end if m.detailMeta.text = awCutText(detailMetaText, 70) setPlotSafe(moviePlot(item)) setPlotLinesVisible(true) if moviePct >= 95 then m.detailButton.text = "OK = ASSISTIR NOVAMENTE" if isMovieFavorite(item) then m.footer.text = "OK = assistir novamente | * = remover favorito | Voltar = filmes" else m.footer.text = "OK = assistir novamente | * = favoritar | Voltar = filmes" end if else if moviePct > 0 then m.detailButton.text = "OK = CONTINUAR" if isMovieFavorite(item) then m.footer.text = "OK = continuar | * = remover favorito | Voltar = filmes" else m.footer.text = "OK = continuar | * = favoritar | Voltar = filmes" end if else m.detailButton.text = "OK = ASSISTIR" if isMovieFavorite(item) then m.footer.text = "OK = assistir | * = remover favorito | Voltar = filmes" else m.footer.text = "OK = assistir | * = favoritar | Voltar = filmes" end if end if if m.detailFavButton <> invalid then if isMovieFavorite(item) then m.detailFavButton.text = "* = REMOVER FAVORITO" else m.detailFavButton.text = "* = FAVORITAR" end if end if end sub ' PATCH v1.8.242 - prompt premium para retomar ou iniciar do começo. ' PATCH v1.8.245 - evita conexão duplicada no XUI e reforça retomar episódios. function awResumeTimeText(seconds as Integer) as String if seconds <= 0 then return "0 min" hours = Int(seconds / 3600) mins = Int((seconds MOD 3600) / 60) if hours > 0 then if mins > 0 then return hours.ToStr() + "h " + mins.ToStr() + "min" end if return hours.ToStr() + "h" end if if mins < 1 then mins = 1 return mins.ToStr() + "min" end function function awResumeSecondsFor(kind as String, item as Dynamic) as Integer if item = invalid then return 0 if kind = "movie" then return movieResumeSeconds(item) return episodeResumeSeconds(item) end function sub renderResumePrompt() if m.resumePromptGroup = invalid then return m.resumePromptGroup.visible = true if m.resumePromptChoice = invalid then m.resumePromptChoice = 0 if m.resumePromptChoice < 0 then m.resumePromptChoice = 0 if m.resumePromptChoice > 1 then m.resumePromptChoice = 1 if m.resumePromptBtnBg1 <> invalid then if m.resumePromptChoice = 0 then m.resumePromptBtnBg1.color = "0xDC2626FF" else m.resumePromptBtnBg1.color = "0x210A0EEE" end if end if if m.resumePromptBtnBg2 <> invalid then if m.resumePromptChoice = 1 then m.resumePromptBtnBg2.color = "0xDC2626FF" else m.resumePromptBtnBg2.color = "0x210A0EEE" end if end if if m.resumePromptBtn1 <> invalid then if m.resumePromptChoice = 0 then m.resumePromptBtn1.color = "0xFFFFFFFF" else m.resumePromptBtn1.color = "0xFDE2E2FF" end if end if if m.resumePromptBtn2 <> invalid then if m.resumePromptChoice = 1 then m.resumePromptBtn2.color = "0xFFFFFFFF" else m.resumePromptBtn2.color = "0xFDE2E2FF" end if end if m.top.setFocus(true) end sub sub hideResumePrompt(restoreMode as Boolean) if m.resumePromptGroup <> invalid then m.resumePromptGroup.visible = false m.resumePromptVisible = false if restoreMode = true then if m.resumePromptPrevMode <> invalid and m.resumePromptPrevMode <> "" then m.mode = m.resumePromptPrevMode end if end if m.top.setFocus(true) end sub sub showResumePrompt(kind as String, item as Dynamic, title as String, url as String, fmt as String, returnMode as String) resumeSeconds = awResumeSecondsFor(kind, item) if resumeSeconds <= 30 then return m.resumePromptPrevMode = m.mode m.resumePromptVisible = true m.resumePromptChoice = 0 m.resumePendingKind = kind m.resumePendingItem = item m.resumePendingTitle = title if m.resumePendingTitle = invalid or m.resumePendingTitle = "" then m.resumePendingTitle = "Episódio" m.resumePendingUrl = "" if url <> invalid then m.resumePendingUrl = url.ToStr() m.resumePendingFmt = "" if fmt <> invalid then m.resumePendingFmt = fmt.ToStr() m.resumePendingReturnMode = returnMode m.mode = "resume_prompt" if m.resumePromptTitle <> invalid then m.resumePromptTitle.text = "CONTINUAR ASSISTINDO?" if m.resumePromptMessage <> invalid then if kind = "movie" then m.resumePromptMessage.text = awCutText(m.resumePendingTitle, 62) else m.resumePromptMessage.text = awCutText(m.resumePendingTitle, 62) end if end if if m.resumePromptTime <> invalid then m.resumePromptTime.text = "Você parou em " + awResumeTimeText(resumeSeconds) + ". Escolha uma opção para reproduzir." end if if m.resumePromptBtn1 <> invalid then m.resumePromptBtn1.text = "CONTINUAR" if m.resumePromptBtn2 <> invalid then m.resumePromptBtn2.text = "DO INÍCIO" if m.resumePromptHint <> invalid then m.resumePromptHint.text = "Use ← → e OK • Voltar = cancelar" renderResumePrompt() end sub function awMaybeShowResumePrompt(kind as String, item as Dynamic, title as String, url as String, fmt as String, returnMode as String) as Boolean if awResumeSecondsFor(kind, item) > 30 then showResumePrompt(kind, item, title, url, fmt, returnMode) return true end if return false end function sub awStartResumePending(startFromBeginning as Boolean) if m.resumePromptVisible <> true then return kind = m.resumePendingKind item = m.resumePendingItem title = m.resumePendingTitle url = m.resumePendingUrl fmt = m.resumePendingFmt returnMode = m.resumePendingReturnMode hideResumePrompt(false) m.resumeForceStart = startFromBeginning if kind = "movie" then m.currentMovie = item m.returnModeAfterPlayer = returnMode if m.returnModeAfterPlayer = invalid or m.returnModeAfterPlayer = "" then m.returnModeAfterPlayer = "movie_detail" m.pendingTitle = title m.currentPlayerEpg = "" if url <> invalid and url <> "" then playVideo(title, url, fmt) return end if mid = movieIdOf(item) if mid = "" then m.resumeForceStart = false showNotice("ERRO", "Filme sem ID para reprodução.") return end if clearMovieUi() m.mode = "loading_movie_play" m.menuTitle.text = "ABRINDO FILME" m.menu1.text = title if startFromBeginning then m.menu2.text = "Iniciando do começo..." else m.menu2.text = "Continuando de onde parou..." end if m.menu3.text = "" m.menu4.text = "" m.menu5.text = "" m.menu6.text = "" m.menu7.text = "" m.footer.text = "Aguarde..." playUrl = m.apiBase + "/api/roku/vod-play.php?token=" + m.token + "&id=" + mid m.apiGetTask.url = playUrl m.apiGetTask.control = "RUN" return end if if kind = "series" then m.currentEpisode = item m.returnModeAfterPlayer = returnMode if m.returnModeAfterPlayer = invalid or m.returnModeAfterPlayer = "" then m.returnModeAfterPlayer = "series_episodes" m.pendingTitle = title if url <> invalid and url <> "" then playVideo(title, url, fmt) return end if epId = episodeIdOf(item) if epId = "" then m.resumeForceStart = false showNotice("ERRO", "Episódio sem ID.") return end if m.mode = "loading_episode_play" m.menuTitle.text = "EPISÓDIO" if startFromBeginning then m.menu1.text = "Iniciando episódio do começo..." else m.menu1.text = "Continuando episódio de onde parou..." end if m.footer.text = "Aguarde..." m.apiGetTask.url = m.apiBase + "/api/roku/series-play.php?token=" + m.token + "&episode_id=" + epId m.apiGetTask.control = "RUN" return end if m.resumeForceStart = false end sub sub cancelResumePrompt() m.resumeForceStart = false hideResumePrompt(true) end sub sub playMovie() item = m.currentMovie if item = invalid then return m.currentMovie = item title = itemTitle(item) streamUrl = "" if item.stream_url <> invalid then streamUrl = item.stream_url if streamUrl = "" and item.url <> invalid then streamUrl = item.url if streamUrl = "" and item.play_url <> invalid then streamUrl = item.play_url if streamUrl <> "" then m.returnModeAfterPlayer = "movie_detail" m.pendingTitle = title m.currentPlayerEpg = "" finalMovieUrl = awPreferDirectVodUrlForMkv(item, streamUrl, "movie") finalMovieFmt = awMediaFormatFromItem(item, "auto") if awMaybeShowResumePrompt("movie", item, title, finalMovieUrl, finalMovieFmt, "movie_detail") then return m.resumeForceStart = false playVideo(title, finalMovieUrl, finalMovieFmt) return end if mid = movieIdOf(item) if mid = "" then showNotice("ERRO", "Filme sem ID para reprodução.") return end if m.returnModeAfterPlayer = "movie_detail" m.pendingTitle = title m.currentPlayerEpg = "" if awMaybeShowResumePrompt("movie", item, title, "", "", "movie_detail") then return m.resumeForceStart = false clearMovieUi() m.mode = "loading_movie_play" m.menuTitle.text = "ABRINDO FILME" m.menu1.text = title m.menu2.text = "Buscando link de reprodução..." m.menu3.text = "" m.menu4.text = "" m.menu5.text = "" m.menu6.text = "" m.menu7.text = "" m.footer.text = "Aguarde..." url = m.apiBase + "/api/roku/vod-play.php?token=" + m.token + "&id=" + mid m.apiGetTask.url = url m.apiGetTask.control = "RUN" end sub sub hideListInfoUi() if m.listInfoGroup <> invalid then m.listInfoGroup.visible = false end sub function awReadRegistryValue(key as String) as String sec = CreateObject("roRegistrySection", "awplayer") if sec.Exists(key) then v = sec.Read(key) if v <> invalid then return v.ToStr() end if return "" end function sub renderListInfoSelection() ' v1.8.214: foco agora muda a cor do próprio botão, porque o retângulo de foco ficava atrás do fundo. if m.menuIndex < 0 then m.menuIndex = 0 if m.menuIndex > 2 then m.menuIndex = 2 if m.listInfoOptionFocus <> invalid then for i = 0 to 2 if m.listInfoOptionFocus[i] <> invalid then m.listInfoOptionFocus[i].visible = false end for end if if m.listInfoOptionBg <> invalid then for i = 0 to 2 if m.listInfoOptionBg[i] <> invalid then if i = m.menuIndex then if i = 1 then m.listInfoOptionBg[i].color = "0xDC2626FF" else m.listInfoOptionBg[i].color = "0xDC2626FF" end if else m.listInfoOptionBg[i].color = "0x140D10FF" end if end if end for end if end sub sub showListInfoScreen() applyAppBackground() hideActivationText() hideHomeBanner() hideStartupBanner() hideAllHomeRows() clearMovieUi() setPremiumHomeVisible(false) hideLoginUi() m.mode = "list_info" m.menuIndex = 0 if m.listInfoGroup <> invalid then m.listInfoGroup.visible = true userTxt = awReadRegistryValue("username") if userTxt = "" then userTxt = "--" expRaw = awReadRegistryValue("expiry_raw") expTxt = awFormatExpiryDate(expRaw) if expTxt = "" then expTxt = "--" devTxt = awVisibleDeviceCode() if m.listInfoTitle <> invalid then m.listInfoTitle.text = m.listTitleText if m.listInfoUser <> invalid then m.listInfoUser.text = m.listUsernameLabel + " " + userTxt if m.listInfoExpiry <> invalid then m.listInfoExpiry.text = m.listExpirationLabel + " " + expTxt if m.listInfoDevice <> invalid then m.listInfoDevice.text = m.listDeviceLabel + " " + devTxt if m.listInfoVersion <> invalid then m.listInfoVersion.text = m.listVersionLabel + " " + m.appVersionDisplay if m.listInfoOptionLabel <> invalid then if m.listInfoOptionLabel[0] <> invalid then m.listInfoOptionLabel[0].text = m.listChangeLoginText if m.listInfoOptionLabel[1] <> invalid then m.listInfoOptionLabel[1].text = m.listDeleteText if m.listInfoOptionLabel[2] <> invalid then m.listInfoOptionLabel[2].text = m.listBackText end if renderListInfoSelection() if m.footer <> invalid then m.footer.visible = false end sub sub deleteSavedListAndLogin() sec = CreateObject("roRegistrySection", "awplayer") keys = ["token", "activation_code", "device_code", "username", "password", "dns_id", "dns_name", "dns_base_url", "provider_code", "expiry_raw", "home_continue_movies"] for each k in keys if sec.Exists(k) then sec.Delete(k) end for sec.Flush() m.token = "" showLoginScreen() if m.loginStatus <> invalid then m.loginStatus.text = "Lista excluída. Faça login novamente." end if end sub sub selectListInfo() if m.menuIndex = 0 then showLoginScreen() else if m.menuIndex = 1 then deleteSavedListAndLogin() else showHome() end if end sub sub selectHome() if m.menuIndex = 0 then loadLiveCategories() else if m.menuIndex = 1 then loadMovieCategories() else if m.menuIndex = 2 then loadSeriesCategories() else if m.menuIndex = 3 then loadSportsEvents() else if m.menuIndex = 4 then showListInfoScreen() else if m.menuIndex = 5 then showAccountMenu() else if m.menuIndex = 6 then m.modeBeforeBannerConfig = "home" loadAppBanners() end if end sub sub showNotice(title as String, message as String) hideActivationText() clearMovieUi() setPremiumHomeVisible(false) m.mode = "notice" m.menuTitle.text = title m.menu1.text = message m.menu2.text = "" m.menu3.text = "" m.menu4.text = "" m.menu5.text = "" m.menu6.text = "" m.menu7.text = "" m.footer.text = "Voltar = retornar" end sub function channelCategoryListPageSize() as Integer return 10 end function function currentChannelCategoryListAbsIndex() as Integer return (m.page * channelCategoryListPageSize()) + m.menuIndex end function function channelCategoryCountOf(item as Dynamic) as String if item = invalid then return "" if item.category_count <> invalid then return item.category_count.ToStr() if item.count <> invalid then return item.count.ToStr() if item.total <> invalid then return item.total.ToStr() if item.streams_count <> invalid then return item.streams_count.ToStr() return "" end function sub setChannelSidebarVisible(show as Boolean) if m.channelSideShade <> invalid then m.channelSideShade.visible = show if m.channelSideTitle <> invalid then m.channelSideTitle.visible = show if m.channelCatBg <> invalid then for each x in m.channelCatBg if x <> invalid then x.visible = false end for end if if m.channelCatFocus <> invalid then for each x in m.channelCatFocus if x <> invalid then x.visible = false end for end if if m.channelCatLabels <> invalid then for each x in m.channelCatLabels if x <> invalid then x.visible = false end for end if if m.channelCatCounts <> invalid then for each x in m.channelCatCounts if x <> invalid then x.visible = false end for end if end sub sub renderChannelSidebar() if m.channelCategories = invalid then return setChannelSidebarVisible(true) totalCats = m.channelCategories.Count() if totalCats <= 0 then return selectedAbs = m.channelCategoryIndex if selectedAbs < 0 then selectedAbs = 0 if selectedAbs >= totalCats then selectedAbs = totalCats - 1 m.channelCategoryIndex = selectedAbs startAt = 0 if selectedAbs > 6 then startAt = selectedAbs - 3 if startAt + 10 > totalCats then startAt = totalCats - 10 if startAt < 0 then startAt = 0 categoryHasFocus = (m.channelFocusArea = "categories") for i = 0 to 9 if m.channelCatBg[i] <> invalid then m.channelCatBg[i].visible = false if m.channelCatFocus[i] <> invalid then m.channelCatFocus[i].visible = false if m.channelCatLabels[i] <> invalid then m.channelCatLabels[i].text = "" m.channelCatLabels[i].visible = false end if if m.channelCatCounts[i] <> invalid then m.channelCatCounts[i].text = "" m.channelCatCounts[i].visible = false end if end for for i = 0 to 9 idx = startAt + i if idx < totalCats then cat = m.channelCategories[idx] name = itemTitle(cat) countTxt = channelCategoryCountOf(cat) if name = invalid or name = "" then name = "Categoria" if UCase(name) = "FAVORITOS" then name = "Favoritos" if m.channelCatBg[i] <> invalid then m.channelCatBg[i].visible = true if idx = selectedAbs and categoryHasFocus = false then m.channelCatBg[i].color = "0x3A0B12E8" else m.channelCatBg[i].color = "0x230A0EE8" end if end if if idx = selectedAbs and m.channelCatFocus[i] <> invalid then m.channelCatFocus[i].visible = true if categoryHasFocus then m.channelCatFocus[i].color = "0xFFD43BFF" else m.channelCatFocus[i].color = "0xFB7185FF" end if end if if m.channelCatLabels[i] <> invalid then m.channelCatLabels[i].text = awCutText(name, 27) if idx = selectedAbs then m.channelCatLabels[i].color = "0xFFFFFFFF" else m.channelCatLabels[i].color = "0xFFE4E6FF" end if m.channelCatLabels[i].visible = true end if if m.channelCatCounts[i] <> invalid then m.channelCatCounts[i].text = countTxt if idx = selectedAbs then m.channelCatCounts[i].color = "0xFFFFFFFF" else m.channelCatCounts[i].color = "0xF5B8BDFF" end if m.channelCatCounts[i].visible = true end if end if end for end sub sub hideChannelLiveUiOnly() setChannelSidebarVisible(false) setChannelStreamUiVisible(false) hideListEpg() end sub sub clearChannelModernUi() hideActivationText() hideMovieLoading() hideAllHomeRows() hideHomeBanner() clearMovieUi() setMovieSidebarVisible(false) setSeriesSidebarVisible(false) setChannelSidebarVisible(false) setChannelStreamUiVisible(false) hideListEpg() end sub function channelStreamsPageSize() as Integer ' Layout IBO Copa: 4 colunas x 2 linhas return 8 end function function currentChannelStreamAbsIndex() as Integer return (m.page * channelStreamsPageSize()) + m.menuIndex end function function channelLogoOf(item as Dynamic) as String if item = invalid then return "" if item.stream_icon <> invalid and item.stream_icon <> "" then return item.stream_icon.ToStr() if item.logo <> invalid and item.logo <> "" then return item.logo.ToStr() if item.image <> invalid and item.image <> "" then return item.image.ToStr() if item.cover <> invalid and item.cover <> "" then return item.cover.ToStr() return "" end function sub setChannelStreamUiVisible(show as Boolean) if m.channelListPaneBg <> invalid then m.channelListPaneBg.visible = show if m.channelListTitle <> invalid then m.channelListTitle.visible = show if m.channelItemBg <> invalid then for each x in m.channelItemBg if x <> invalid then x.visible = false end for end if if m.channelItemFocus <> invalid then for each x in m.channelItemFocus if x <> invalid then x.visible = false end for end if if m.channelItemLogo <> invalid then for each x in m.channelItemLogo if x <> invalid then x.visible = false end for end if if m.channelItemLabel <> invalid then for each x in m.channelItemLabel if x <> invalid then x.text = "" x.visible = false end if end for end if if m.channelPreviewBox <> invalid then m.channelPreviewBox.visible = show if m.channelPreviewTag <> invalid then m.channelPreviewTag.visible = show if m.channelPreviewLogo <> invalid then m.channelPreviewLogo.visible = show if m.channelPreviewTitle <> invalid then m.channelPreviewTitle.visible = show if m.channelPreviewMeta <> invalid then m.channelPreviewMeta.visible = show if m.channelPreviewInfoBg <> invalid then m.channelPreviewInfoBg.visible = show if m.channelPreviewNow <> invalid then m.channelPreviewNow.visible = show if m.channelPreviewNext <> invalid then m.channelPreviewNext.visible = show if m.channelPreviewChip1 <> invalid then m.channelPreviewChip1.visible = show if m.channelPreviewChip2 <> invalid then m.channelPreviewChip2.visible = show if m.channelPreviewChip3 <> invalid then m.channelPreviewChip3.visible = show if m.channelPreviewHint1 <> invalid then m.channelPreviewHint1.visible = show if m.channelPreviewHint2 <> invalid then m.channelPreviewHint2.visible = show if m.channelPreviewHint3 <> invalid then m.channelPreviewHint3.visible = show end sub function awTrimTextSafe(v as Dynamic) as String if v = invalid then return "" s = v.ToStr() while Len(s) > 0 and (Left(s, 1) = " " or Left(s, 1) = Chr(9) or Left(s, 1) = Chr(10) or Left(s, 1) = Chr(13)) s = Mid(s, 2) end while while Len(s) > 0 and (Right(s, 1) = " " or Right(s, 1) = Chr(9) or Right(s, 1) = Chr(10) or Right(s, 1) = Chr(13)) s = Left(s, Len(s) - 1) end while return s end function sub setChannelPreviewCombinedEpg(txt as String) nowText = "Agora: sem informação" nextText = "Depois: sem informação" if txt <> invalid and txt <> "" then sep = Instr(1, txt, "|") if sep > 0 then nowText = awTrimTextSafe(Left(txt, sep - 1)) nextText = awTrimTextSafe(Mid(txt, sep + 1)) else nowText = txt end if end if if m.channelPreviewNow <> invalid then m.channelPreviewNow.text = nowText if m.channelPreviewNext <> invalid then m.channelPreviewNext.text = nextText end sub sub renderChannelStreamPreview(item as Dynamic) if m.channelPreviewBox <> invalid then m.channelPreviewBox.visible = true if m.channelPreviewTag <> invalid then m.channelPreviewTag.visible = true if m.channelPreviewLogo <> invalid then m.channelPreviewLogo.visible = true if m.channelPreviewTitle <> invalid then m.channelPreviewTitle.visible = true if m.channelPreviewMeta <> invalid then m.channelPreviewMeta.visible = true if m.channelPreviewInfoBg <> invalid then m.channelPreviewInfoBg.visible = true if m.channelPreviewNow <> invalid then m.channelPreviewNow.visible = true if m.channelPreviewNext <> invalid then m.channelPreviewNext.visible = true if m.channelPreviewChip1 <> invalid then m.channelPreviewChip1.visible = true if m.channelPreviewChip2 <> invalid then m.channelPreviewChip2.visible = true if m.channelPreviewChip3 <> invalid then m.channelPreviewChip3.visible = true if m.channelPreviewHint1 <> invalid then m.channelPreviewHint1.visible = true if m.channelPreviewHint2 <> invalid then m.channelPreviewHint2.visible = true if m.channelPreviewHint3 <> invalid then m.channelPreviewHint3.visible = true title = "Selecione um canal" meta = "" logoUri = awAppLogoUri() if m.mode = "favorites" then meta = "Seus canais favoritados" if m.channelPreviewHint3 <> invalid then m.channelPreviewHint3.text = "Left = categorias Voltar = categorias" if m.channelPreviewHint2 <> invalid then m.channelPreviewHint2.text = "* = remover" else if m.currentCategoryName <> invalid and m.currentCategoryName <> "" then meta = "Categoria: " + m.currentCategoryName else meta = "Lista de canais" end if if m.channelPreviewHint3 <> invalid then m.channelPreviewHint3.text = "Left = categorias Voltar = sair" if m.channelPreviewHint2 <> invalid then m.channelPreviewHint2.text = "* = favorito" end if if item <> invalid then title = awCutText(itemTitle(item), 30) logoCandidate = channelLogoOf(item) if logoCandidate <> "" then logoUri = logoCandidate idx = currentChannelStreamAbsIndex() + 1 total = 0 if m.items <> invalid then total = m.items.Count() meta = "Canal " + idx.ToStr() + " de " + total.ToStr() + " - " + meta if isFavorite(item) then meta = meta + " - FAVORITO" txt = epgTextForPlayer(item) if txt <> "" then setChannelPreviewCombinedEpg(txt) else if m.channelPreviewNow <> invalid then m.channelPreviewNow.text = "Agora: carregando programação..." if m.channelPreviewNext <> invalid then m.channelPreviewNext.text = "Depois: aguarde..." loadEpgForItem(item) end if else if m.channelPreviewNow <> invalid then m.channelPreviewNow.text = "Agora: sem informação" if m.channelPreviewNext <> invalid then m.channelPreviewNext.text = "Depois: sem informação" end if if m.channelPreviewLogo <> invalid then m.channelPreviewLogo.uri = logoUri if m.channelPreviewTitle <> invalid then m.channelPreviewTitle.text = title if m.channelPreviewMeta <> invalid then m.channelPreviewMeta.text = meta end sub sub hideChannelPreviewIboCopa() if m.channelPreviewBox <> invalid then m.channelPreviewBox.visible = false if m.channelPreviewTag <> invalid then m.channelPreviewTag.visible = false if m.channelPreviewLogo <> invalid then m.channelPreviewLogo.visible = false if m.channelPreviewTitle <> invalid then m.channelPreviewTitle.visible = false if m.channelPreviewMeta <> invalid then m.channelPreviewMeta.visible = false if m.channelPreviewInfoBg <> invalid then m.channelPreviewInfoBg.visible = false if m.channelPreviewNow <> invalid then m.channelPreviewNow.visible = false if m.channelPreviewNext <> invalid then m.channelPreviewNext.visible = false if m.channelPreviewChip1 <> invalid then m.channelPreviewChip1.visible = false if m.channelPreviewChip2 <> invalid then m.channelPreviewChip2.visible = false if m.channelPreviewChip3 <> invalid then m.channelPreviewChip3.visible = false if m.channelPreviewHint1 <> invalid then m.channelPreviewHint1.visible = false if m.channelPreviewHint2 <> invalid then m.channelPreviewHint2.visible = false if m.channelPreviewHint3 <> invalid then m.channelPreviewHint3.visible = false end sub sub applyChannelIboCopaLayout() if m.channelSideShade <> invalid then m.channelSideShade.width = 285 m.channelSideShade.height = 555 m.channelSideShade.translation = [28, 138] m.channelSideShade.color = "0x1B080BE8" m.channelSideShade.visible = true end if if m.channelSideTitle <> invalid then m.channelSideTitle.text = "CATEGORIAS" m.channelSideTitle.width = 250 m.channelSideTitle.translation = [48, 92] m.channelSideTitle.color = "0xFFD43BFF" m.channelSideTitle.visible = true end if catY = [156, 207, 258, 309, 360, 411, 462, 513, 564, 615] for i = 0 to 9 if m.channelCatBg[i] <> invalid then m.channelCatBg[i].width = 255 m.channelCatBg[i].height = 43 m.channelCatBg[i].translation = [42, catY[i]] end if if m.channelCatFocus[i] <> invalid then m.channelCatFocus[i].width = 255 m.channelCatFocus[i].height = 43 m.channelCatFocus[i].translation = [42, catY[i]] end if if m.channelCatLabels[i] <> invalid then m.channelCatLabels[i].width = 190 m.channelCatLabels[i].height = 36 m.channelCatLabels[i].translation = [57, catY[i] + 4] end if if m.channelCatCounts[i] <> invalid then m.channelCatCounts[i].width = 43 m.channelCatCounts[i].height = 36 m.channelCatCounts[i].translation = [245, catY[i] + 4] end if end for if m.channelListPaneBg <> invalid then m.channelListPaneBg.width = 920 m.channelListPaneBg.height = 555 m.channelListPaneBg.translation = [325, 138] m.channelListPaneBg.color = "0x17090CE8" m.channelListPaneBg.visible = true end if if m.channelListTitle <> invalid then m.channelListTitle.width = 880 m.channelListTitle.height = 44 m.channelListTitle.translation = [340, 92] m.channelListTitle.color = "0xFFD43BFF" m.channelListTitle.visible = true end if end sub sub clearChannelCardNodes() for i = 0 to 7 if m.channelItemBg[i] <> invalid then m.channelItemBg[i].visible = false if m.channelItemFocus[i] <> invalid then m.channelItemFocus[i].visible = false if m.channelItemLogo[i] <> invalid then m.channelItemLogo[i].visible = false if m.channelItemLabel[i] <> invalid then m.channelItemLabel[i].text = "" m.channelItemLabel[i].visible = false end if end for end sub sub renderChannelPaneMessage(title as String, message as String, showCategories as Boolean) ' Tela exclusiva de TV ao vivo: remove Home, login e informações da lista ' antes de desenhar categorias/canais, evitando qualquer overlay. m.homeInfoPending = false setPremiumHomeVisible(false) hideLoginUi() hideListInfoUi() clearHome() clearChannelModernUi() clearMainMenuTexts() hideChannelPreviewIboCopa() clearChannelCardNodes() if m.footer <> invalid then m.footer.text = "" if showCategories then setChannelSidebarVisible(true) applyChannelIboCopaLayout() renderChannelSidebar() else setChannelSidebarVisible(false) if m.channelListPaneBg <> invalid then m.channelListPaneBg.width = 1185 m.channelListPaneBg.height = 555 m.channelListPaneBg.translation = [48, 138] m.channelListPaneBg.color = "0x17090CE8" m.channelListPaneBg.visible = true end if if m.channelListTitle <> invalid then m.channelListTitle.width = 1120 m.channelListTitle.translation = [70, 92] m.channelListTitle.color = "0xFFD43BFF" m.channelListTitle.visible = true end if end if if m.channelListTitle <> invalid then m.channelListTitle.text = title if m.channelItemLabel[0] <> invalid then m.channelItemLabel[0].width = 820 m.channelItemLabel[0].height = 100 if showCategories then m.channelItemLabel[0].translation = [372, 310] else m.channelItemLabel[0].translation = [225, 310] end if m.channelItemLabel[0].horizAlign = "center" m.channelItemLabel[0].vertAlign = "center" m.channelItemLabel[0].wrap = true m.channelItemLabel[0].color = "0xFFF5F5FF" m.channelItemLabel[0].text = message m.channelItemLabel[0].visible = true end if m.top.setFocus(true) end sub sub moveChannelSidebarSelection(delta as Integer) if m.channelCategories = invalid or m.channelCategories.Count() = 0 then return nextIndex = m.channelCategoryIndex + delta if nextIndex < 0 then nextIndex = m.channelCategories.Count() - 1 if nextIndex >= m.channelCategories.Count() then nextIndex = 0 m.channelCategoryIndex = nextIndex if m.mode = "live_categories" then m.page = Int(nextIndex / channelCategoryListPageSize()) m.menuIndex = nextIndex - (m.page * channelCategoryListPageSize()) renderChannelCategoriesModern() else renderList() end if end sub sub openChannelCategoryFromSidebar() if m.channelCategories = invalid or m.channelCategories.Count() = 0 then return idxSel = m.channelCategoryIndex if idxSel < 0 or idxSel >= m.channelCategories.Count() then return item = m.channelCategories[idxSel] m.channelLoadedCategoryIndex = idxSel m.currentCategoryName = itemTitle(item) cid = "" if item.category_id <> invalid then cid = item.category_id.ToStr() if item.id <> invalid and cid = "" then cid = item.id.ToStr() m.currentCategoryId = cid if cid = "__favorites__" then showFavorites() return end if if cid = "" then m.channelFocusArea = "categories" renderChannelPaneMessage("TV AO VIVO", "Esta categoria não possui um identificador válido.", true) return end if m.channelFocusArea = "grid" m.menuIndex = 0 m.page = 0 m.items = [] m.mode = "loading_live_streams_modern" renderChannelPaneMessage(UCase(m.currentCategoryName), "Carregando canais desta categoria...", true) m.footer.text = "AGUARDE • VOLTAR = categorias" url = m.apiBase + "/api/roku/live-streams.php?token=" + m.token + "&category_id=" + cid m.apiGetTask.control = "STOP" m.apiGetTask.url = url m.apiGetTask.control = "RUN" end sub sub renderChannelStreamsModern() setPremiumHomeVisible(false) hideLoginUi() hideListInfoUi() clearHome() clearChannelModernUi() clearMainMenuTexts() setChannelStreamUiVisible(true) hideChannelPreviewIboCopa() clearChannelCardNodes() if m.channelFocusArea <> "categories" and m.channelFocusArea <> "grid" then m.channelFocusArea = "grid" totalItems = 0 if m.items <> invalid then totalItems = m.items.Count() pageSize = channelStreamsPageSize() maxPage = 0 if totalItems > 0 then maxPage = Int((totalItems - 1) / pageSize) if m.page < 0 then m.page = 0 if m.page > maxPage then m.page = maxPage remain = totalItems - (m.page * pageSize) if remain > pageSize then remain = pageSize if remain < 0 then remain = 0 if m.menuIndex < 0 then m.menuIndex = 0 if remain > 0 and m.menuIndex >= remain then m.menuIndex = remain - 1 if m.menuIndex < 0 then m.menuIndex = 0 setChannelSidebarVisible(true) applyChannelIboCopaLayout() renderChannelSidebar() baseTitle = "TV AO VIVO" if m.mode = "favorites" then baseTitle = "FAVORITOS" else if m.currentCategoryName <> invalid and m.currentCategoryName <> "" then baseTitle = UCase(m.currentCategoryName) end if if m.channelListTitle <> invalid then if totalItems > 0 then m.channelListTitle.text = baseTitle + " • PÁGINA " + (m.page + 1).ToStr() + "/" + (maxPage + 1).ToStr() else m.channelListTitle.text = baseTitle end if end if labels = [m.menu1, m.menu2, m.menu3, m.menu4, m.menu5, m.menu6, m.menu7] for each lb in labels if lb <> invalid then lb.visible = false end for cardX = [345, 568, 791, 1014, 345, 568, 791, 1014] cardY = [164, 164, 164, 164, 422, 422, 422, 422] for i = 0 to 7 if m.channelItemBg[i] <> invalid then m.channelItemBg[i].width = 202 m.channelItemBg[i].height = 226 m.channelItemBg[i].translation = [cardX[i], cardY[i]] m.channelItemBg[i].color = "0x230A0EF5" m.channelItemBg[i].visible = false end if if m.channelItemFocus[i] <> invalid then m.channelItemFocus[i].width = 214 m.channelItemFocus[i].height = 238 m.channelItemFocus[i].translation = [cardX[i] - 6, cardY[i] - 6] m.channelItemFocus[i].color = "0xFFD43BFF" m.channelItemFocus[i].visible = false end if if m.channelItemLogo[i] <> invalid then m.channelItemLogo[i].width = 126 m.channelItemLogo[i].height = 126 m.channelItemLogo[i].translation = [cardX[i] + 38, cardY[i] + 18] m.channelItemLogo[i].loadDisplayMode = "scaleToFit" m.channelItemLogo[i].visible = false end if if m.channelItemLabel[i] <> invalid then m.channelItemLabel[i].width = 184 m.channelItemLabel[i].height = 66 m.channelItemLabel[i].translation = [cardX[i] + 9, cardY[i] + 151] m.channelItemLabel[i].horizAlign = "center" m.channelItemLabel[i].vertAlign = "center" m.channelItemLabel[i].wrap = true m.channelItemLabel[i].color = "0xFFF5F5FF" m.channelItemLabel[i].text = "" m.channelItemLabel[i].visible = false end if end for if totalItems <= 0 then if m.channelItemLabel[0] <> invalid then m.channelItemLabel[0].width = 820 m.channelItemLabel[0].height = 100 m.channelItemLabel[0].translation = [372, 310] m.channelItemLabel[0].horizAlign = "center" m.channelItemLabel[0].vertAlign = "center" m.channelItemLabel[0].wrap = true if m.mode = "favorites" then m.channelItemLabel[0].text = "Nenhum canal favorito. Escolha outra categoria na lateral." else m.channelItemLabel[0].text = "Nenhum canal encontrado nesta categoria." end if m.channelItemLabel[0].visible = true end if m.footer.text = "CIMA/BAIXO = categorias • DIREITA/OK = abrir • VOLTAR = Home" return end if startIndex = m.page * pageSize gridHasFocus = (m.channelFocusArea = "grid") for i = 0 to 7 idx = startIndex + i if idx < totalItems then item = m.items[idx] title = awCutText(itemTitle(item), 28) if isFavorite(item) then title = title + " [FAV]" if m.channelItemBg[i] <> invalid then m.channelItemBg[i].visible = true if m.channelItemFocus[i] <> invalid then m.channelItemFocus[i].visible = (gridHasFocus and i = m.menuIndex) if m.channelItemLogo[i] <> invalid then logoUri = channelLogoOf(item) if logoUri = "" then logoUri = awAppLogoUri() m.channelItemLogo[i].uri = logoUri m.channelItemLogo[i].visible = true end if if m.channelItemLabel[i] <> invalid then m.channelItemLabel[i].text = title if gridHasFocus and i = m.menuIndex then m.channelItemLabel[i].color = "0xFFFFFFFF" else m.channelItemLabel[i].color = "0xFFE4E6FF" end if m.channelItemLabel[i].visible = true end if end if end for if m.movieSelectedTitle <> invalid then m.movieSelectedTitle.visible = false if m.channelFocusArea = "categories" then m.footer.text = "CIMA/BAIXO = categorias • DIREITA/OK = abrir • VOLTAR = Home" else if m.mode = "favorites" then m.footer.text = "SETAS = navegar • ESQUERDA = categorias • OK = assistir • * = remover" else m.footer.text = "SETAS = navegar • ESQUERDA = categorias • OK = assistir • * = favorito" end if m.top.setFocus(true) end sub sub renderChannelCategoriesModern() setPremiumHomeVisible(false) hideLoginUi() hideListInfoUi() clearHome() clearChannelModernUi() clearMainMenuTexts() hideChannelPreviewIboCopa() clearChannelCardNodes() if m.items <> invalid then m.channelCategories = m.items m.channelFocusArea = "categories" setChannelSidebarVisible(true) applyChannelIboCopaLayout() totalCats = 0 if m.channelCategories <> invalid then totalCats = m.channelCategories.Count() if totalCats <= 0 then renderChannelPaneMessage("TV AO VIVO", "Nenhuma categoria encontrada.", false) m.footer.text = "VOLTAR = Home" return end if if m.channelCategoryIndex < 0 then m.channelCategoryIndex = 0 if m.channelCategoryIndex >= totalCats then m.channelCategoryIndex = totalCats - 1 m.page = Int(m.channelCategoryIndex / channelCategoryListPageSize()) m.menuIndex = m.channelCategoryIndex - (m.page * channelCategoryListPageSize()) renderChannelSidebar() selectedItem = m.channelCategories[m.channelCategoryIndex] selectedName = itemTitle(selectedItem) if selectedName = invalid or selectedName = "" then selectedName = "Categoria" if m.channelListTitle <> invalid then m.channelListTitle.text = "TV AO VIVO" if m.channelItemLabel[0] <> invalid then m.channelItemLabel[0].width = 820 m.channelItemLabel[0].height = 130 m.channelItemLabel[0].translation = [372, 285] m.channelItemLabel[0].horizAlign = "center" m.channelItemLabel[0].vertAlign = "center" m.channelItemLabel[0].wrap = true m.channelItemLabel[0].color = "0xFFF5F5FF" m.channelItemLabel[0].text = "Escolha uma categoria na lateral." + Chr(10) + Chr(10) + "OK ou DIREITA para abrir: " + selectedName m.channelItemLabel[0].visible = true end if m.footer.text = "CIMA/BAIXO = categorias • DIREITA/OK = abrir • VOLTAR = Home" m.top.setFocus(true) end sub sub moveChannelCategoryMenu(delta as Integer) if m.items = invalid or m.items.Count() = 0 then return totalCats = m.items.Count() absIndex = currentChannelCategoryListAbsIndex() + delta if absIndex < 0 then absIndex = totalCats - 1 if absIndex >= totalCats then absIndex = 0 m.page = Int(absIndex / channelCategoryListPageSize()) m.menuIndex = absIndex - (m.page * channelCategoryListPageSize()) renderChannelCategoriesModern() end sub sub showChannelLoading(title as String) clearChannelModernUi() clearMainMenuTexts() applyAppBackground() awApplyLoadingBase(title, "Buscando canais da categoria...") end sub sub selectChannelCategoryModern() openChannelCategoryFromSidebar() end sub sub loadLiveCategories() ' A Home consulta o vencimento usando a mesma ApiGetTask. ' Cancela essa consulta antes de iniciar categorias para a nova URL realmente executar. m.homeInfoPending = false if m.apiGetTask <> invalid then m.apiGetTask.control = "STOP" m.mode = "loading_categories" m.channelFocusArea = "categories" setPremiumHomeVisible(false) hideListInfoUi() clearMainMenuTexts() renderChannelPaneMessage("TV AO VIVO", "Carregando categorias...", false) if m.footer <> invalid then m.footer.text = "AGUARDE" url = m.apiBase + "/api/roku/live-categories.php?token=" + m.token m.apiGetTask.url = url m.apiGetTask.control = "RUN" end sub sub loadLiveStreams(categoryId as String) m.mode = "loading_streams" m.currentCategoryId = categoryId m.menuTitle.text = "CANAIS" m.menu1.text = "Carregando canais..." m.menu2.text = "" m.menu3.text = "" m.menu4.text = "" m.menu5.text = "" m.menu6.text = "" m.menu7.text = "" m.footer.text = "Aguarde..." url = m.apiBase + "/api/roku/live-streams.php?token=" + m.token + "&category_id=" + categoryId m.apiGetTask.control = "STOP" m.apiGetTask.url = url m.apiGetTask.control = "RUN" end sub function awFriendlyApiError(msg as Dynamic) as String texto = "" if msg <> invalid then texto = msg.ToStr() if texto = "" then return "Não foi possível conectar ao painel. Tente novamente." low = LCase(texto) if Instr(1, low, "http -3") > 0 or Instr(1, low, "http 3") > 0 or Instr(1, low, "http-3") > 0 then return "Falha ao conectar ao painel. Verifique internet/IP e tente novamente." end if if Instr(1, low, "timed out") > 0 or Instr(1, low, "timeout") > 0 then return "Tempo esgotado ao conectar no painel. Tente novamente." end if if Instr(1, low, "dispositivo inativo") > 0 or Instr(1, low, "aparelho inativo") > 0 then return "Dispositivo inativo. Fale com seu provedor para reativar." end if if Instr(1, low, "dispositivo bloqueado") > 0 or Instr(1, low, "aparelho bloqueado") > 0 then return "Dispositivo bloqueado. Fale com seu provedor para liberar." end if if Instr(1, low, "dispositivo expirado") > 0 or Instr(1, low, "aparelho vencido") > 0 then return "Dispositivo expirado. Renove seu acesso com o provedor." end if if Instr(1, low, "sem retorno da api") > 0 then return "Sem resposta do painel. Verifique conexão e tente novamente." end if return texto end function sub setLoginStatusSafe(msg as Dynamic) if m.loginStatus <> invalid then m.loginStatus.text = awFriendlyApiError(msg) end if end sub function isLoginModeActive() as Boolean if m.mode = invalid then return false s = m.mode.ToStr() if s = "login" then return true if s = "login_provider" then return true if s = "login_user" then return true if s = "login_password" then return true if s = "loading_login_dns" then return true if s = "loading_dns_list" then return true if s = "login_loading" then return true if s = "doing_login" then return true return false end function sub showLoginFriendlyError(msg as Dynamic) hideMovieLoading() showLoginScreen() setLoginStatusSafe(msg) end sub function awTextHas(hay as String, needle as String) as Boolean if hay = invalid then return false return Instr(1, LCase(hay), LCase(needle)) > 0 end function function shouldForceLoginForApiError() as Boolean if m.mode = invalid then return false s = m.mode.ToStr() ' Nunca apagar token por falha em banners/config/login. if s = "banner_config" then return false if s = "home_start" then return false if s = "home" then return false if isLoginModeActive() then return false ' Limpa sessão só em áreas que dependem de token real. if s = "validating_token" then return true if s = "checking_device" then return true if s = "loading_categories" then return true if s = "loading_streams" then return true if s = "loading_live_streams_modern" then return true if s = "live_categories" then return true if s = "live_streams" then return true if s = "loading_movie_categories" then return true if s = "loading_movies" then return true if s = "loading_movies_auto" then return true if s = "loading_movies_inline" then return true if s = "movie_categories" then return true if s = "movie_grid" then return true if s = "loading_series_categories" then return true if s = "loading_series_inline" then return true if s = "loading_series_modern" then return true if s = "loading_series_search_results" then return true if s = "series_categories" then return true if s = "series_grid_modern" then return true return false end function function isAwSessionInvalidMessage(msg as Dynamic) as Boolean if msg = invalid then return false s = msg.ToStr() if awTextHas(s, "sess") then return true if awTextHas(s, "token") then return true if awTextHas(s, "login novamente") then return true if awTextHas(s, "faça login") then return true if awTextHas(s, "faca login") then return true if awTextHas(s, "aparelho bloqueado") then return true if awTextHas(s, "dispositivo inativo") then return true if awTextHas(s, "dispositivo bloqueado") then return true if awTextHas(s, "dispositivo expirado") then return true if awTextHas(s, "inativo") then return true if awTextHas(s, "bloqueado") then return true if awTextHas(s, "provedor removido") then return true if awTextHas(s, "dns removido") then return true if awTextHas(s, "sem retorno da api") then return true if awTextHas(s, "http -3") then return true if awTextHas(s, "http 3") then return true if awTextHas(s, "http-3") then return true return false end function sub forceLoginFromApiError(msg as Dynamic) if isLoginModeActive() then showLoginFriendlyError(msg) return end if texto = "Sessão expirada. Faça login novamente." if msg <> invalid and msg.ToStr() <> "" then texto = msg.ToStr() ' Limpa sessão/token salvo para evitar voltar quebrado. sec = CreateObject("roRegistrySection", "awplayer") if sec.Exists("token") then sec.Delete("token") if sec.Exists("activation_code") then sec.Delete("activation_code") if sec.Exists("device_code") then sec.Delete("device_code") if sec.Exists("username") then sec.Delete("username") if sec.Exists("password") then sec.Delete("password") if sec.Exists("dns_id") then sec.Delete("dns_id") if sec.Exists("provider_code") then sec.Delete("provider_code") sec.Flush() m.token = "" m.loginProviderCode = "" m.loginUsername = "" m.loginPassword = "" m.loginField = 0 hideMovieLoading() clearHome() clearMovieUi() setPremiumHomeVisible(false) hideAllHomeRows() hideHomeBanner() clearMainMenuTexts() showLoginScreen() if m.loginStatus <> invalid then m.loginStatus.text = texto end sub sub onApiGetResult() res = m.apiGetTask.result requestUrl = "" if res <> invalid and res.request_url <> invalid then requestUrl = res.request_url.ToStr() ' A consulta de vencimento da Home usa a mesma Task. ' Ela pode terminar depois que o usuário já entrou em TV ao vivo; nesse caso, ' apenas salva os dados e nunca redesenha elementos da Home sobre a tela de canais. if requestUrl <> "" and Instr(1, requestUrl, "/home.php") > 0 then m.homeInfoPending = false if res.success = true then saveLoginSessionSafe(res) if m.mode = "home" or m.mode = "home_start" then renderHomeExpiration() return end if ' Ignora respostas antigas que não pertencem à tela de canais atualmente carregada. if m.mode = "loading_categories" and requestUrl <> "" and Instr(1, requestUrl, "/live-categories.php") = 0 then return if m.mode = "loading_live_streams_modern" and requestUrl <> "" and Instr(1, requestUrl, "/live-streams.php") = 0 then return ' onApiGetResult_login_guard_v138 if res <> invalid and isLoginModeActive() then if res.success <> invalid and res.success <> true then if res.message <> invalid then showLoginFriendlyError(res.message) return else if res.error <> invalid then showLoginFriendlyError(res.error) return end if end if end if if res = invalid then if isLoginModeActive() then showLoginFriendlyError("Sem retorno da API.") return end if if shouldForceLoginForApiError() then forceLoginFromApiError("Sem retorno da API. Faça login novamente.") return end if else if res.success <> invalid and res.success <> true then msg = "" if res.message <> invalid then msg = res.message.ToStr() if shouldForceLoginForApiError() and isAwSessionInvalidMessage(msg) then forceLoginFromApiError(msg) return end if else if res.error <> invalid then msg = res.error.ToStr() if shouldForceLoginForApiError() and isAwSessionInvalidMessage(msg) then forceLoginFromApiError(msg) return end if end if if m.mode = "banner_config" then okConfig = false if res <> invalid and res.success = true then data = res.data if data = invalid then data = res applyBannerConfig(data) m.bannerConfigLoaded = true okConfig = true end if backMode = "login" if m.modeBeforeBannerConfig <> invalid and m.modeBeforeBannerConfig <> "" then backMode = m.modeBeforeBannerConfig end if m.modeBeforeBannerConfig = invalid if backMode = "home_start" then m.mode = "home" showHome() else if backMode = "login" then m.mode = "login" forceRefreshVisibleAssets() applyDynamicTextsToScreen() renderLoginScreen() if okConfig then if m.loginStatus <> invalid then m.loginStatus.text = "Dados atualizados do painel." else errMsg = "Não foi possível atualizar agora." if res <> invalid and res.message <> invalid then errMsg = res.message.ToStr() if m.loginStatus <> invalid then m.loginStatus.text = errMsg end if else if backMode = "home" then m.mode = "home" forceRefreshVisibleAssets() showHome() else if backMode = "account" then m.mode = "account" showAccountMenu() if okConfig then m.footer.text = "Aparência atualizada. Volte para Home para conferir." else errMsg = "Não foi possível atualizar agora." if res <> invalid and res.message <> invalid then errMsg = res.message.ToStr() m.footer.text = errMsg end if else m.mode = backMode forceRefreshVisibleAssets() end if return end if if m.mode = "login" then if res = invalid or res.success <> true then msg = "Falha ao carregar DNS" if res <> invalid and res.message <> invalid then msg = res.message m.loginDnsList = [] setLoginStatusSafe(msg) renderLoginScreen() return end if data = res.data if data = invalid then data = res m.loginDnsList = awDnsToArray(data) if m.loginDnsList.Count() > 0 then m.loginStatus.text = "Digite usuário e senha." else m.loginStatus.text = "Nenhum DNS ativo no painel." end if renderLoginScreen() return end if if m.seriesLoadingMore = true and (res = invalid or res.success <> true) then m.seriesLoadingMore = false m.seriesHasMore = false m.seriesPendingLazyPage = invalid if m.mode = "series_grid_modern" then renderSeriesGridModern() return end if if m.mode = "loading_series_inline" and (res = invalid or res.success <> true) then msg = "Falha ao carregar séries" if res <> invalid and res.message <> invalid then msg = res.message if isInvalidProviderMessage(msg) then logoutToLogin(msg) else hideMovieLoading() m.mode = "series_grid_modern" m.seriesFocusArea = "categories" m.items = [] m.menuIndex = 0 m.page = 0 renderSeriesGridModern() showCatalogHint(msg) if m.footer <> invalid then m.footer.text = "CIMA/BAIXO = categorias • DIREITA/OK = tentar novamente • VOLTAR = Home" end if return end if if res = invalid or res.success <> true then msg = "Falha ao carregar" if res <> invalid and res.message <> invalid then msg = res.message if isInvalidProviderMessage(msg) then logoutToLogin(msg) else showNotice("ERRO", msg) end if return end if data = res.data if data <> invalid and data.success <> invalid and data.success <> true then msg = "Não foi possível carregar." if data.message <> invalid then msg = data.message.ToStr() if shouldForceLoginForApiError() or isAwSessionInvalidMessage(msg) then forceLoginFromApiError(msg) else showNotice("ERRO", msg) end if return end if if m.seriesLoadingMore = true then m.seriesLoadingMore = false arrMore = normalizeArray(data) if arrMore.Count() = 0 then m.seriesHasMore = false else addedMore = appendUniqueSeriesItems(arrMore) if addedMore > 0 then if m.seriesPendingLazyPage <> invalid then m.seriesLazyPage = m.seriesPendingLazyPage m.seriesGridItems = m.items else ' Se a API ignorar page/limit e devolver repetido, para para não ficar em loop. m.seriesHasMore = false end if if arrMore.Count() < seriesBatchLimit() then m.seriesHasMore = false end if m.seriesPendingLazyPage = invalid if m.mode = "series_grid_modern" then renderSeriesGridModern() return end if if m.mode = "loading_categories" then arr = normalizeArray(data) if arr.count() = 0 then logoutToLogin("Sem retorno da API. Faça login novamente.") return end if ' Mantém Favoritos como a primeira opção dentro de TV ao vivo. ' A lista é local por aparelho e o contador é atualizado ao favoritar/remover. liveCategories = [] liveCategories.Push({ category_id: "__favorites__", category_name: "FAVORITOS", category_count: getFavorites().Count().ToStr() }) for each liveCat in arr liveCatId = "" if liveCat.category_id <> invalid then liveCatId = liveCat.category_id.ToStr() if liveCatId = "" and liveCat.id <> invalid then liveCatId = liveCat.id.ToStr() if liveCatId <> "__favorites__" then liveCategories.Push(liveCat) end for arr = liveCategories m.mode = "live_categories" m.items = arr m.channelCategories = arr m.channelCategoryIndex = 0 m.channelLoadedCategoryIndex = 0 m.channelFocusArea = "categories" m.menuIndex = 0 m.page = 0 renderChannelCategoriesModern() return else if m.mode = "loading_live_streams_modern" then hideMovieLoading() arr = normalizeArray(data) m.mode = "live_streams" m.items = arr m.menuIndex = 0 m.page = 0 m.channelFocusArea = "grid" if m.currentCategoryName = invalid then m.currentCategoryName = "Canais" renderList() return else if m.mode = "loading_streams" then arr = normalizeArray(data) if arr.count() = 0 then logoutToLogin("Sem retorno da API. Faça login novamente.") return end if m.mode = "live_streams" m.items = arr m.menuIndex = 0 m.page = 0 m.menuTitle.text = "CANAIS | " + m.currentCategoryName renderList() m.footer.text = "OK = assistir | * = favoritar | EPG embaixo | Voltar = categorias" else if m.mode = "loading_play" then awStopLiveOpenTimer() m.liveOpenUsingFastUrl = false if data.stream_url <> invalid and data.stream_url <> "" then resolvedLiveUrl = data.stream_url.ToStr() if m.liveFallbackStreamId <> invalid and m.liveFallbackStreamId <> "" and m.liveResolvedUrls <> invalid then m.liveResolvedUrls[m.liveFallbackStreamId] = resolvedLiveUrl end if fmt = "hls" if data.stream_format <> invalid then fmt = data.stream_format playTitle = m.pendingTitle if m.liveFallbackTitle <> invalid and m.liveFallbackTitle <> "" then playTitle = m.liveFallbackTitle m.liveReconnectAttempts = 0 playVideo(playTitle, resolvedLiveUrl, fmt) else showNotice("ERRO NO LINK", "A API nao retornou stream_url para este canal.") end if else if m.mode = "loading_movie_categories" then arr = normalizeArray(data) continueOption = { category_id: "__movie_continue__", category_name: "CONTINUAR ASSISTINDO" } searchOption = { category_id: "__search__", category_name: "BUSCAR FILMES" } favOption = { category_id: "__movie_favorites__", category_name: "FAVORITOS DE FILMES" } arrWithSearch = [] arrWithSearch.Push(continueOption) arrWithSearch.Push(searchOption) arrWithSearch.Push(favOption) for each catItem in arr arrWithSearch.Push(catItem) end for arr = arrWithSearch m.movieCategories = arr if arr.count() = 0 then showNotice("SEM CATEGORIAS", "Nenhuma categoria de filme retornou da API.") return end if m.mode = "movie_categories" m.movieFocusArea = "categories" m.items = arr m.movieCategories = arr m.menuIndex = 0 m.page = 0 renderMovieCategoriesModern() return else if m.mode = "loading_movies" then hideMovieLoading() arr = normalizeArray(data) if arr.count() = 0 then logoutToLogin("Sem retorno da API. Faça login novamente.") return end if m.mode = "movie_grid" m.movieFocusArea = "grid" m.items = arr m.menuIndex = 0 m.page = 0 renderMovieGrid() else if m.mode = "loading_movie_search_results" then hideKeyboard() arr = normalizeArray(data) if arr.Count() = 0 then showNotice("BUSCAR FILMES", "Nenhum filme encontrado para: " + m.searchQuery) return end if hideKeyboard() m.mode = "movie_grid" m.movieFocusArea = "grid" m.items = arr m.currentMovieCategoryName = "Busca: " + m.searchQuery m.menuIndex = 0 m.page = 0 renderMovieGrid() else if m.mode = "loading_series_modern" or m.mode = "loading_series_inline" then hideMovieLoading() arr = normalizeArray(data) m.items = [] m.seriesLoadedKeys = {} appendUniqueSeriesItems(arr) if arr.Count() < seriesBatchLimit() then m.seriesHasMore = false if m.items.Count() = 0 then m.mode = "series_grid_modern" m.seriesFocusArea = "grid" m.page = 0 m.menuIndex = 0 renderSeriesGridModern() if m.movieSelectedTitle <> invalid then m.movieSelectedTitle.text = "Nenhuma série nesta categoria" m.movieSelectedTitle.visible = true end if return end if m.mode = "series_grid_modern" m.seriesFocusArea = "grid" m.seriesGridItems = m.items m.seriesGridPage = 0 m.seriesGridIndex = 0 m.page = 0 m.menuIndex = 0 renderSeriesGridModern() return else if m.mode = "loading_series_categories" then arr = normalizeArray(data) if arr.Count() = 0 then showNotice("SÉRIES", "Nenhuma categoria de série retornou da API.") return end if searchSeriesOption = { category_id: "__search_series__", category_name: "BUSCAR SÉRIES" } continueSeriesOption = { category_id: "__series_continue__", category_name: "CONTINUAR SÉRIES" } favSeriesOption = { category_id: "__series_favorites__", category_name: "FAVORITOS DE SÉRIES" } arrWithFav = [] arrWithFav.Push(searchSeriesOption) arrWithFav.Push(continueSeriesOption) arrWithFav.Push(favSeriesOption) for each serCat in arr arrWithFav.Push(serCat) end for arr = arrWithFav m.mode = "series_categories" m.seriesFocusArea = "categories" m.items = arr m.seriesCategories = arr m.page = 0 m.menuIndex = 0 renderSeriesCategoriesModern() return else if m.mode = "loading_series_list" then arr = normalizeArray(data) if arr.Count() = 0 then showNotice("SÉRIES", "Nenhuma série retornou nesta categoria.") return end if m.mode = "series_grid" m.items = arr m.page = 0 m.menuIndex = 0 renderSeriesGrid() else if m.mode = "loading_series_search_results" then hideKeyboard() arr = normalizeArray(data) if arr.Count() = 0 then showNotice("BUSCAR SÉRIES", "Nenhuma série encontrada para: " + m.searchQuery) return end if m.mode = "series_grid_modern" m.seriesFocusArea = "grid" m.items = arr m.seriesGridItems = arr m.currentSeriesCategoryName = "Busca: " + m.searchQuery m.currentSeriesCategoryId = "__search_series__" m.seriesLoadingMore = false m.seriesHasMore = false m.menuIndex = 0 m.page = 0 renderSeriesGridModern() return else if m.mode = "loading_series_info" then prepareSeriesSeasons(data) else if m.mode = "loading_episode_play" then if data.stream_url <> invalid and data.stream_url <> "" then fmt = awMediaFormatFromItem(m.currentEpisode, "mp4") if data.stream_format <> invalid then fmt = data.stream_format if data.container_extension <> invalid then fmt = data.container_extension m.returnModeAfterPlayer = "series_episodes" epTitle = episodeTitleOf(m.currentEpisode) if epTitle = invalid or epTitle = "" then epTitle = "Episódio" end if playVideo(epTitle, awPreferDirectVodUrlForMkv(m.currentEpisode, data.stream_url, "series"), fmt) else if data.url <> invalid and data.url <> "" then m.returnModeAfterPlayer = "series_episodes" fmt = awMediaFormatFromItem(m.currentEpisode, "mp4") if data.stream_format <> invalid then fmt = data.stream_format if data.container_extension <> invalid then fmt = data.container_extension epTitle = episodeTitleOf(m.currentEpisode) if epTitle = invalid or epTitle = "" then epTitle = "Episódio" end if playVideo(epTitle, awPreferDirectVodUrlForMkv(m.currentEpisode, data.url, "series"), fmt) else showNotice("EPISÓDIO", "API não retornou link do episódio.") end if else if m.mode = "loading_movie_play" then if data.stream_url <> invalid and data.stream_url <> "" then fmt = awMediaFormatFromItem(m.currentMovie, "mp4") if data.stream_format <> invalid then fmt = data.stream_format if data.container_extension <> invalid then fmt = data.container_extension playVideo(m.pendingTitle, awPreferDirectVodUrlForMkv(m.currentMovie, data.stream_url, "movie"), fmt) else showNotice("ERRO NO LINK", "A API nao retornou stream_url para este filme.") end if end if end sub function normalizeArray(data as Dynamic) as Object if data = invalid then return [] if type(data) = "roArray" then return data if data.data <> invalid and type(data.data) = "roArray" then return data.data if data.items <> invalid and type(data.items) = "roArray" then return data.items if data.categories <> invalid and type(data.categories) = "roArray" then return data.categories if data.streams <> invalid and type(data.streams) = "roArray" then return data.streams return [] end function sub selectCategory() item = currentItem() if item = invalid then return m.currentCategoryName = itemTitle(item) cid = "" if item.category_id <> invalid then cid = item.category_id.ToStr() if item.id <> invalid then cid = item.id.ToStr() if cid = "" then showNotice("ERRO", "Categoria sem ID.") return end if loadLiveStreams(cid) end sub function awTrimLiveBase(baseUrl as String) as String cleanBase = baseUrl while Len(cleanBase) > 0 and Right(cleanBase, 1) = "/" cleanBase = Left(cleanBase, Len(cleanBase) - 1) end while return cleanBase end function function awBuildFastLiveUrl(item as Dynamic) as String sid = streamIdOf(item) if sid = "" then return "" baseUrl = awReadRegistryValue("dns_base_url") userName = awReadRegistryValue("username") userPass = awReadRegistryValue("password") ' Compatibilidade com APIs que já devolvem as credenciais junto ao canal. if baseUrl = "" and item.base_url <> invalid then baseUrl = item.base_url.ToStr() if baseUrl = "" and item.dns_base_url <> invalid then baseUrl = item.dns_base_url.ToStr() if baseUrl = "" and item.server_url <> invalid then baseUrl = item.server_url.ToStr() if userName = "" and item.username <> invalid then userName = item.username.ToStr() if userPass = "" and item.password <> invalid then userPass = item.password.ToStr() if baseUrl = "" or userName = "" or userPass = "" then return "" baseUrl = awTrimLiveBase(baseUrl) if baseUrl = "" then return "" ' Xtream/XUI normalmente aceita HLS direto nesta rota. Assim evitamos uma ' consulta extra ao play.php antes de entregar o canal ao Video node. return baseUrl + "/live/" + userName + "/" + userPass + "/" + sid + ".m3u8" end function sub awStopLiveOpenTimer() if m.liveOpenTimer <> invalid then m.liveOpenTimer.control = "stop" end sub sub awStopLiveReconnectTimer() if m.liveReconnectTimer <> invalid then m.liveReconnectTimer.control = "stop" end sub sub awStopLiveRestartDelayTimer() if m.liveRestartDelayTimer <> invalid then m.liveRestartDelayTimer.control = "stop" end sub sub awStopLiveStableTimer() if m.liveStableTimer <> invalid then m.liveStableTimer.control = "stop" end sub sub onLiveStableTimer() if m.mode <> "player" or isLivePlayer() = false then return if m.video = invalid then return stateText = "" if m.video.state <> invalid then stateText = m.video.state.ToStr() if stateText = "playing" then m.liveReconnectAttempts = 0 end sub sub awScheduleLiveReconnect() if m.mode <> "player" then return if isLivePlayer() = false then return if m.liveRestartPending = true then return awStopLiveStableTimer() if m.liveReconnectTimer = invalid then return m.liveReconnectTimer.control = "stop" m.liveReconnectTimer.control = "start" end sub sub awShowLiveRecovering() m.liveRecoveringVisible = true if m.playerOverlay <> invalid then m.playerOverlay.visible = true if m.playerTitle <> invalid then m.playerTitle.text = "Reconectando canal..." m.playerTitle.visible = true end if if m.playerHint <> invalid then m.playerHint.text = "Sinal instável. O app está tentando recuperar automaticamente." m.playerHint.visible = true end if end sub sub awHideLiveRecoveringIfNeeded() if isLivePlayer() = false then return if m.liveRecoveringVisible = true then m.liveRecoveringVisible = false hidePlayerOverlay() if m.playerTitle <> invalid and m.currentLiveTitle <> invalid then m.playerTitle.text = m.currentLiveTitle if m.playerHint <> invalid then m.playerHint.text = "OK = informações | Voltar = lista de canais" end if end sub sub awRestartCurrentLiveStream() if m.mode <> "player" then return if m.currentLiveUrl = invalid or m.currentLiveUrl = "" then return if m.video = invalid then return if m.liveRestartPending = true then return ' Fecha e libera a conexão antiga antes de abrir outra. ' Isso evita o XUI contar 2 ou 3 sessões do mesmo aparelho. m.liveRestartPending = true awShowLiveRecovering() awStopLiveReconnectTimer() awStopLiveRestartDelayTimer() m.video.control = "stop" m.video.visible = false m.video.content = invalid if m.liveRestartDelayTimer <> invalid then m.liveRestartDelayTimer.control = "start" else onLiveRestartDelayTimer() end if m.top.setFocus(true) end sub sub onLiveRestartDelayTimer() if m.liveRestartPending <> true then return if m.mode <> "player" or isLivePlayer() = false then m.liveRestartPending = false return end if if m.video = invalid then m.liveRestartPending = false return end if if m.currentLiveUrl = invalid or m.currentLiveUrl = "" then m.liveRestartPending = false return end if content = CreateObject("roSGNode", "ContentNode") content.url = m.currentLiveUrl content.title = m.currentLiveTitle if content.hasField("live") then content.live = true content.streamformat = awContentStreamFormat(m.currentLiveUrl, m.currentLiveFmt, true) awApplyPlaybackHeaders(content, m.currentLiveUrl, true) m.liveRestartPending = false m.video.content = content awForceVideoAudioMax() m.video.visible = true m.video.control = "play" m.top.setFocus(true) end sub sub onLiveReconnectTimer() if m.mode <> "player" then return if isLivePlayer() = false then return if m.video = invalid then return currentState = "" if m.video.state <> invalid then currentState = m.video.state.ToStr() if currentState = "playing" then return if m.liveReconnectAttempts = invalid then m.liveReconnectAttempts = 0 if m.liveMaxReconnectAttempts = invalid then m.liveMaxReconnectAttempts = 3 if m.liveReconnectAttempts >= m.liveMaxReconnectAttempts then awStopLiveReconnectTimer() showPlayerOverlay(m.currentLiveTitle) if m.playerHint <> invalid then m.playerHint.text = "Conexão instável. Pressione Voltar e abra o canal novamente." m.playerHint.visible = true end if return end if m.liveReconnectAttempts = m.liveReconnectAttempts + 1 awRestartCurrentLiveStream() end sub sub awRequestResolvedLiveUrl() if m.mode <> "player" then return if m.liveFallbackRequested = true then return if m.liveFallbackStreamId = invalid or m.liveFallbackStreamId = "" then return m.liveFallbackRequested = true m.liveOpenUsingFastUrl = false awStopLiveOpenTimer() awStopLiveReconnectTimer() awStopLiveStableTimer() ' Para a tentativa direta e pede ao painel o link alternativo apenas quando ' a rota HLS rápida realmente não iniciou. if m.video <> invalid then m.video.control = "stop" m.video.visible = false m.video.content = invalid end if m.mode = "loading_play" clearAllTextForPlayer() m.menuTitle.text = "ABRINDO CANAL" m.menu1.text = m.liveFallbackTitle m.menu2.text = "Tentando rota alternativa..." m.footer.text = "Aguarde..." resolveUrl = m.apiBase + "/api/roku/play.php?token=" + m.token + "&type=live&id=" + m.liveFallbackStreamId + "&ext=m3u8" m.apiGetTask.control = "STOP" m.apiGetTask.url = resolveUrl m.apiGetTask.control = "RUN" end sub sub onLiveOpenTimeout() if m.mode <> "player" then return if isLivePlayer() = false then return if m.liveOpenUsingFastUrl <> true then return awRequestResolvedLiveUrl() end sub sub selectStream() item = currentItem() if item = invalid then return title = itemTitle(item) m.currentChannelItem = item m.currentPlayerEpg = epgTextForPlayer(item) if m.currentPlayerEpg = "" then m.currentPlayerEpg = "Agora: carregando programação..." loadEpgForItem(item) end if sid = streamIdOf(item) m.pendingTitle = title m.liveFallbackStreamId = sid m.liveFallbackTitle = title m.liveFallbackRequested = false m.liveOpenUsingFastUrl = false m.liveReconnectAttempts = 0 awStopLiveStableTimer() m.liveRestartPending = false awStopLiveRestartDelayTimer() m.currentLiveUrl = "" m.currentLiveFmt = "hls" m.currentLiveTitle = title streamUrl = "" streamFromResolvedCache = false ' 1) URL já entregue pela API: abre sem consulta extra. if item.stream_url <> invalid then streamUrl = item.stream_url.ToStr() if streamUrl = "" and item.url <> invalid then streamUrl = item.url.ToStr() if streamUrl = "" and item.play_url <> invalid then streamUrl = item.play_url.ToStr() ' 2) URL resolvida anteriormente nesta sessão: reabrir fica imediato. if streamUrl = "" and sid <> "" and m.liveResolvedUrls <> invalid then cachedLiveUrl = m.liveResolvedUrls[sid] if cachedLiveUrl <> invalid then streamUrl = cachedLiveUrl.ToStr() streamFromResolvedCache = true end if end if ' 3) Monta a rota HLS padrão do Xtream usando dados já salvos no login. ' Isso elimina o pedido ao play.php na maioria dos servidores. if streamUrl = "" then streamUrl = awBuildFastLiveUrl(item) end if ' Qualquer URL ainda não confirmada pelo resolvedor recebe um limite curto. ' Se não tocar, o app busca a rota alternativa automaticamente. if streamUrl <> "" and sid <> "" and streamFromResolvedCache = false then m.liveOpenUsingFastUrl = true end if m.returnModeAfterPlayer = m.mode setChannelStreamUiVisible(false) setChannelSidebarVisible(false) clearMainMenuTexts() if m.movieSelectedTitle <> invalid then m.movieSelectedTitle.visible = false if streamUrl <> "" then playVideo(title, streamUrl, "hls") return end if if sid = "" then showNotice("ERRO", "Canal sem ID para reprodução.") return end if ' Sem credenciais suficientes para montar a URL: usa o resolvedor do painel. m.liveFallbackRequested = true m.mode = "loading_play" m.pendingTitle = title setPremiumHomeVisible(false) m.menuTitle.text = "ABRINDO CANAL" m.menu1.text = title m.menu2.text = "Buscando link de reprodução..." m.menu3.text = "" m.menu4.text = "" m.menu5.text = "" m.menu6.text = "" m.menu7.text = "" m.footer.text = "Aguarde..." playApiUrl = m.apiBase + "/api/roku/play.php?token=" + m.token + "&type=live&id=" + sid + "&ext=m3u8" m.apiGetTask.control = "STOP" m.apiGetTask.url = playApiUrl m.apiGetTask.control = "RUN" end sub sub onEpgTimeout() if m.mode = "player" and isLivePlayer() then m.currentPlayerEpg = "Agora: sem informação | Depois: sem informação" if m.playerEpg <> invalid then m.playerEpg.text = m.currentPlayerEpg m.playerEpg.visible = true end if if m.playerOverlay <> invalid then m.playerOverlay.visible = true if m.playerTitle <> invalid then m.playerTitle.visible = true if m.playerHint <> invalid then m.playerHint.visible = true else if m.mode = "player" then if m.currentPlayerEpg = "EPG: carregando programação..." or m.currentPlayerEpg = "" then m.currentPlayerEpg = "" if m.playerEpg <> invalid then m.playerEpg.text = "" m.playerEpg.visible = false end if hidePlayerOverlay() end if else if m.mode = "live_streams" or m.mode = "favorites" then if m.epgNow <> invalid then m.epgNow.text = "Agora: sem informação" if m.epgNext <> invalid then m.epgNext.text = "Depois: sem informação" end if end sub sub prefetchSportsChannelCatalog() if m.token = invalid or m.token = "" then return if m.sportsChannelResolverTask = invalid then return if m.sportsCatalogPrefetching = true then return if m.sportsChannelCatalog <> invalid and m.sportsChannelCatalog.Count() > 0 then return m.sportsCatalogPrefetching = true m.sportsChannelResolverTask.control = "STOP" m.sportsChannelResolverTask.apiBase = m.apiBase m.sportsChannelResolverTask.token = m.token m.sportsChannelResolverTask.channelName = "" m.sportsChannelResolverTask.returnCatalog = true m.sportsChannelResolverTask.control = "RUN" end sub function sportsNormalizeChannelName(v as Dynamic) as String if v = invalid then return "" s = UCase(v.ToStr().Trim()) s = s.Replace("Á", "A"): s = s.Replace("À", "A"): s = s.Replace("Â", "A"): s = s.Replace("Ã", "A") s = s.Replace("É", "E"): s = s.Replace("Ê", "E") s = s.Replace("Í", "I"): s = s.Replace("Ó", "O"): s = s.Replace("Ô", "O"): s = s.Replace("Õ", "O") s = s.Replace("Ú", "U"): s = s.Replace("Ç", "C") separators = ["-", "_", ".", "/", "\\", "|", ":", ";", ",", "(", ")", "[", "]", "+", "&"] for each separator in separators s = s.Replace(separator, " ") end for while Instr(1, s, " ") > 0 s = s.Replace(" ", " ") end while return s.Trim() end function function sportsChannelMatchScoreLocal(aliasName as String, streamName as String) as Integer aliasNorm = sportsNormalizeChannelName(aliasName) streamNorm = sportsNormalizeChannelName(streamName) if aliasNorm = "" or streamNorm = "" then return 0 aliasCompact = aliasNorm.Replace(" ", "") streamCompact = streamNorm.Replace(" ", "") if aliasCompact = "GLOBO" or aliasCompact = "TVGLOBO" then if Instr(1, streamCompact, "GLOBONEWS") > 0 or Instr(1, streamNorm, "NEWS") > 0 then return 0 if streamCompact = "GLOBO" then return 155 if streamCompact = "TVGLOBO" then return 153 if Instr(1, streamCompact, "GLOBO") > 0 then return 135 return 0 end if if aliasNorm = streamNorm then return 140 if aliasCompact = streamCompact then return 138 if Instr(1, " " + streamNorm + " ", " " + aliasNorm + " ") > 0 then return 125 if Len(aliasCompact) >= 4 and Instr(1, streamCompact, aliasCompact) > 0 then return 110 if Len(streamCompact) >= 4 and Instr(1, aliasCompact, streamCompact) > 0 then return 82 return 0 end function function sportsFindChannelInCatalog(channelName as String) as Dynamic if m.sportsChannelCatalog = invalid or m.sportsChannelCatalog.Count() = 0 then return invalid bestItem = invalid bestScore = 0 for each streamItem in m.sportsChannelCatalog score = sportsChannelMatchScoreLocal(channelName, itemTitle(streamItem)) if score > bestScore then bestScore = score bestItem = streamItem end if end for if bestItem <> invalid and bestScore >= 70 then return bestItem return invalid end function function sportsDirectUrl(item as Dynamic) as String if item = invalid then return "" keys = ["stream_url", "url", "play_url", "link", "channel_url", "canal_url", "link_canal", "transmissao_url", "m3u8", "hls"] for each key in keys value = item.Lookup(key) if value <> invalid and value.ToStr().Trim() <> "" then return value.ToStr().Trim() end for return "" end function function sportsStreamId(item as Dynamic) as String if item = invalid then return "" keys = ["stream_id", "channel_id", "canal_id", "live_id", "id_canal", "id_stream"] for each key in keys value = item.Lookup(key) if value <> invalid and value.ToStr().Trim() <> "" then return value.ToStr().Trim() end for return "" end function sub playSportsFastItem(channelItem as Dynamic, fallbackTitle as String) if channelItem = invalid then return playItem = channelItem title = fallbackTitle if title = invalid or title = "" then title = itemTitle(channelItem) if itemTitle(playItem) = "" then playItem.name = title m.items = [playItem] m.menuIndex = 0 m.page = 0 m.mode = "sports" hideSportsScreen() selectStream() end sub sub hideSportsScreen() if m.sportsGroup <> invalid then m.sportsGroup.visible = false end sub sub showSportsScreenNodes(show as Boolean) if m.sportsGroup <> invalid then m.sportsGroup.visible = show end sub function sportsAny(item as Dynamic, keys as Object) as String if item = invalid then return "" if keys = invalid then return "" for each key in keys if key <> invalid and key <> "" then value = item.Lookup(key) if value <> invalid and value.ToStr().Trim() <> "" then return value.ToStr().Trim() end if end for return "" end function function sportsValue(item as Dynamic, key1 as String, key2 as String, key3 as String) as String return sportsAny(item, [key1, key2, key3]) end function function sportsCut(v as Dynamic, maxLen as Integer) as String if v = invalid then return "" s = v.ToStr() if Len(s) > maxLen then return Left(s, maxLen - 3) + "..." return s end function function sportsCleanText(v as Dynamic) as String if v = invalid then return "" s = v.ToStr() s = s.Replace(chr(10), " ") s = s.Replace(chr(13), " ") while Instr(1, s, " ") > 0 s = s.Replace(" ", " ") end while return s.Trim() end function function sportsFriendlyDateTime(v as Dynamic) as String s = sportsCleanText(v) if s = "" then return "" ' Formato ISO ou parecido: 2026-06-14 23:00 / 2026-06-14T23:00 if Len(s) >= 10 and Mid(s, 5, 1) = "-" and Mid(s, 8, 1) = "-" then dd = Mid(s, 9, 2) mm = Mid(s, 6, 2) hh = "" if Len(s) >= 16 then hh = Mid(s, 12, 5) if hh <> "" then return dd + "/" + mm + " " + hh return dd + "/" + mm end if ' Formato da API UFC: 14/06/2026, 23:00 commaPos = Instr(1, s, ",") if commaPos > 0 then leftPart = Left(s, commaPos - 1).Trim() rightPart = Mid(s, commaPos + 1).Trim() if Len(leftPart) >= 5 then shortDate = Left(leftPart, 5) if Len(rightPart) >= 5 then return shortDate + " " + Left(rightPart, 5) return shortDate end if end if ' Se vier "hoje 15:00", mantém. low = LCase(s) if Instr(1, low, "hoje") > 0 or Instr(1, low, "aman") > 0 then if Len(s) > 18 then return Left(s, 18) return s end if ' Formato normal dd/mm/yyyy ou dd/mm/yyyy hh:mm if Len(s) >= 10 and Mid(s, 3, 1) = "/" and Mid(s, 6, 1) = "/" then shortDate = Left(s, 5) if Len(s) >= 16 then maybeHour = Mid(s, 12, 5) if Instr(1, maybeHour, ":") > 0 then return shortDate + " " + maybeHour end if return shortDate end if if Len(s) > 18 then return Left(s, 18) return s end function function sportsSmartName(v as Dynamic, maxLen as Integer) as String s = sportsCleanText(v) if s = "" then return "" if Len(s) <= maxLen then return s ' Tenta nome + inicial do sobrenome em vez de reticências. spacePos = Instr(1, s, " ") if spacePos > 0 and spacePos < Len(s) then firstName = Left(s, spacePos - 1) rest = Mid(s, spacePos + 1) if Len(rest) > 0 then compact = firstName + " " + Left(rest, 1) + "." if Len(compact) <= maxLen then return compact end if end if if maxLen <= 3 then return Left(s, maxLen) return Left(s, maxLen - 1) + "..." end function function sportsSmartLeague(v as Dynamic, maxLen as Integer) as String s = sportsCleanText(v) s = s.Replace("Campeonato Brasileiro", "Brasileiro") s = s.Replace("Brasileirão", "Bras.") s = s.Replace("Copa do Mundo", "Copa Mundo") s = s.Replace("Evento co-principal", "Co-principal") s = s.Replace("Evento Principal", "Principal") if Len(s) <= maxLen then return s if maxLen <= 3 then return Left(s, maxLen) return Left(s, maxLen - 1) + "..." end function function sportsChannelName(item as Dynamic) as String if item = invalid then return "" if item.channels <> invalid and type(item.channels) = "roArray" and item.channels.Count() > 0 then ch = item.channels[0] if type(ch) = "roAssociativeArray" and ch.name <> invalid then return ch.name.ToStr() return ch.ToStr() end if return sportsAny(item, ["channel", "canal", "canal_transmissao", "channel_name", "canal_nome", "transmissao", "emissora", "tv", "canal_tv"]) end function function sportsApiUrls() as Object urls = [] if m.token <> invalid and m.token <> "" then urls.Push(m.apiBase + "/api/roku/sports.php?token=" + m.token + "&modality=football") end if if m.sportsUfcUrl <> invalid and m.sportsUfcUrl <> "" then urls.Push(m.sportsUfcUrl) if m.sportsBasketUrl <> invalid and m.sportsBasketUrl <> "" then urls.Push(m.sportsBasketUrl) return urls end function function sportsTabName(index as Integer) as String if index = 1 then return "UFC" if index = 2 then return "Basquete" return "Futebol" end function function sportsModality(item as Dynamic) as String if item = invalid then return "" raw = sportsAny(item, ["modalidade", "sport", "tipo", "category", "categoria"]) low = LCase(raw) if Instr(1, low, "ufc") > 0 or Instr(1, low, "mma") > 0 or Instr(1, low, "luta") > 0 then return "UFC" if Instr(1, low, "basquete") > 0 or Instr(1, low, "basket") > 0 or Instr(1, low, "nba") > 0 then return "Basquete" if Instr(1, low, "futebol") > 0 or Instr(1, low, "football") > 0 or Instr(1, low, "futbol") > 0 then return "Futebol" text = LCase(sportsAny(item, ["competition", "liga", "campeonato", "evento", "event", "league", "title", "name", "nome"])) if Instr(1, text, "ufc") > 0 or Instr(1, text, "mma") > 0 then return "UFC" if Instr(1, text, "nba") > 0 or Instr(1, text, "basquete") > 0 or Instr(1, text, "basket") > 0 then return "Basquete" return "Futebol" end function sub applySportsFilter() filtered = [] selected = sportsTabName(m.sportsTabIndex) if m.sportsAllItems <> invalid then for each item in m.sportsAllItems if sportsModality(item) = selected then filtered.Push(item) end for end if m.sportsItems = filtered m.sportsPage = 0 m.sportsIndex = 0 end sub sub renderSportsTabs() labels = ["Futebol", "UFC", "Basquete"] for i = 0 to 2 if m.sportsTabLabel <> invalid and m.sportsTabLabel[i] <> invalid then m.sportsTabLabel[i].text = labels[i] m.sportsTabLabel[i].visible = true end if if m.sportsTabBg <> invalid and m.sportsTabBg[i] <> invalid then m.sportsTabBg[i].visible = true if i = m.sportsTabIndex then if m.sportsFocusArea = "tabs" then m.sportsTabBg[i].color = "0xDC2626FF" else m.sportsTabBg[i].color = "0x8B151DFF" end if else m.sportsTabBg[i].color = "0x220A0EEE" end if end if end for end sub sub loadSportsEvents() clearHome() clearMovieUi() setPremiumHomeVisible(false) hideAllHomeRows() hideHomeBanner() m.mode = "loading_sports" m.sportsPage = 0 m.sportsIndex = 0 m.sportsTabIndex = 0 m.sportsFocusArea = "cards" showSportsScreenNodes(true) if m.sportsTitle <> invalid then m.sportsTitle.text = "ESPORTES" if m.sportsSubtitle <> invalid then m.sportsSubtitle.text = "Futebol, UFC e Basquete" if m.sportsCountLabel <> invalid then m.sportsCountLabel.text = "" if m.sportsMessage <> invalid then m.sportsMessage.text = "Carregando esportes..." renderSportsTabs() renderSportsScreen() if m.sportsApiTask = invalid then m.mode = "sports" if m.sportsMessage <> invalid then m.sportsMessage.text = "Componente de esportes não encontrado." return end if m.sportsCacheReady = false m.sportsPrefetching = true m.sportsAllItems = [] m.sportsItems = [] m.sportsApiTask.control = "STOP" m.sportsApiTask.url = "" m.sportsApiTask.urls = sportsApiUrls() m.sportsApiTask.days = 1 m.sportsApiTask.control = "RUN" end sub sub onSportsApiResult() res = m.sportsApiTask.result m.sportsPrefetching = false if res = invalid or res.success <> true then if m.mode = "loading_sports" then m.mode = "sports" msg = "Não foi possível carregar os esportes." if res <> invalid and res.message <> invalid then msg = res.message.ToStr() if m.sportsMessage <> invalid then m.sportsMessage.text = msg renderSportsScreen() end if return end if data = res.data arr = [] if data <> invalid and data.items <> invalid and type(data.items) = "roArray" then arr = data.items else if type(data) = "roArray" then arr = data m.sportsAllItems = arr m.sportsCacheItems = arr m.sportsCacheReady = true prefetchSportsChannelCatalog() if m.mode = "loading_sports" then m.mode = "sports" applySportsFilter() renderSportsScreen() end if end sub sub clearSportsCard(i as Integer) if i < 0 or i >= 6 then return if m.sportsCardBg[i] <> invalid then m.sportsCardBg[i].visible = false if m.sportsLeague[i] <> invalid then m.sportsLeague[i].visible = false if m.sportsTime[i] <> invalid then m.sportsTime[i].visible = false if m.sportsHomeLogo[i] <> invalid then m.sportsHomeLogo[i].visible = false if m.sportsAwayLogo[i] <> invalid then m.sportsAwayLogo[i].visible = false if m.sportsHomeName[i] <> invalid then m.sportsHomeName[i].visible = false if m.sportsAwayName[i] <> invalid then m.sportsAwayName[i].visible = false if m.sportsScore[i] <> invalid then m.sportsScore[i].visible = false if m.sportsChannel[i] <> invalid then m.sportsChannel[i].visible = false end sub sub renderSportsScreen() showSportsScreenNodes(true) renderSportsTabs() total = 0 if m.sportsItems <> invalid then total = m.sportsItems.Count() pages = 1 if total > 0 then pages = Int((total - 1) / m.sportsPerPage) + 1 if m.sportsPage < 0 then m.sportsPage = 0 if m.sportsPage >= pages then m.sportsPage = pages - 1 if m.sportsPager <> invalid then m.sportsPager.text = "Página " + (m.sportsPage + 1).ToStr() + "/" + pages.ToStr() if m.sportsCountLabel <> invalid then m.sportsCountLabel.text = total.ToStr() + " eventos" if m.sportsTitle <> invalid then m.sportsTitle.text = "ESPORTES" if m.sportsSubtitle <> invalid then m.sportsSubtitle.text = sportsTabName(m.sportsTabIndex) if m.sportsMessage <> invalid then if total = 0 and m.mode <> "loading_sports" then m.sportsMessage.text = "Nenhum evento encontrado em " + sportsTabName(m.sportsTabIndex) + "." else if total > 0 then if m.sportsFocusArea = "tabs" then m.sportsMessage.text = "Cima/Baixo = modalidade | OK/Direita = cards" else m.sportsMessage.text = "Esquerda na primeira coluna = modalidades | OK = abrir canal" end if end if end if startAt = m.sportsPage * m.sportsPerPage remain = total - startAt if remain > m.sportsPerPage then remain = m.sportsPerPage if m.sportsIndex >= remain then m.sportsIndex = remain - 1 if m.sportsIndex < 0 then m.sportsIndex = 0 for i = 0 to 5 idx = startAt + i if idx >= total then clearSportsCard(i) else item = m.sportsItems[idx] bg = m.sportsCardBg[i] if bg <> invalid then bg.visible = true if i = m.sportsIndex and m.sportsFocusArea <> "tabs" then bg.color = "0x8B151DFF" else bg.color = "0x220A0EEE" end if modality = sportsModality(item) league = sportsAny(item, ["competition", "liga", "campeonato", "evento", "event", "league", "categoria"]) if league = "" then league = sportsAny(item, ["title", "name", "nome"]) if modality <> "" then if league <> "" then league = modality + " • " + league else league = modality end if titleText = sportsAny(item, ["title", "name", "nome", "luta", "fight", "descricao"]) homeName = sportsAny(item, ["team1", "time_casa", "home", "mandante", "casa", "lutador1", "fighter1", "atleta1", "competidor1", "nome1", "player1"]) awayName = sportsAny(item, ["team2", "time_fora", "away", "visitante", "fora", "lutador2", "fighter2", "atleta2", "competidor2", "nome2", "player2"]) if homeName = "" and awayName = "" and titleText <> "" then homeName = titleText awayName = sportsAny(item, ["subtitulo", "subtitle", "categoria", "peso"]) end if homeLogo = sportsAny(item, ["team1_image", "logo_casa", "home_logo", "url_imagem1", "imagem1", "image1", "lutador1_image", "fighter1_image", "logo1"]) awayLogo = sportsAny(item, ["team2_image", "logo_fora", "away_logo", "url_imagem2", "imagem2", "image2", "lutador2_image", "fighter2_image", "logo2"]) timeText = sportsAny(item, ["datetime_label", "data_evento", "time", "hora", "horario", "date", "data", "data_hora"]) statusText = sportsAny(item, ["status", "situacao", "state"]) score1 = sportsAny(item, ["score1", "placar_casa", "home_score"]) score2 = sportsAny(item, ["score2", "placar_fora", "away_score"]) score = "x" if sportsModality(item) = "UFC" then score = "VS" if score1 <> "" or score2 <> "" then score = score1 + " x " + score2 channel = sportsChannelName(item) if statusText <> "" then timeText = timeText + " " + statusText if m.sportsLeague[i] <> invalid then m.sportsLeague[i].text = sportsSmartLeague(league, 34): m.sportsLeague[i].visible = true if m.sportsHomeName[i] <> invalid then m.sportsHomeName[i].text = sportsSmartName(homeName, 18): m.sportsHomeName[i].visible = true if m.sportsAwayName[i] <> invalid then m.sportsAwayName[i].text = sportsSmartName(awayName, 18): m.sportsAwayName[i].visible = true if m.sportsScore[i] <> invalid then m.sportsScore[i].text = score: m.sportsScore[i].visible = true if m.sportsTime[i] <> invalid then m.sportsTime[i].text = sportsFriendlyDateTime(timeText): m.sportsTime[i].visible = true if m.sportsChannel[i] <> invalid then m.sportsChannel[i].text = sportsSmartLeague(channel, 28): m.sportsChannel[i].visible = true if m.sportsHomeLogo[i] <> invalid then m.sportsHomeLogo[i].uri = homeLogo: m.sportsHomeLogo[i].visible = (homeLogo <> "") if m.sportsAwayLogo[i] <> invalid then m.sportsAwayLogo[i].uri = awayLogo: m.sportsAwayLogo[i].visible = (awayLogo <> "") end if end for if m.footer <> invalid then m.footer.visible = false m.footer.text = "" end if m.top.setFocus(true) end sub function currentSportsEvent() as Dynamic if m.sportsItems = invalid then return invalid idx = (m.sportsPage * m.sportsPerPage) + m.sportsIndex if idx < 0 or idx >= m.sportsItems.Count() then return invalid return m.sportsItems[idx] end function function handleSportsKey(key as String) as Boolean if key = "back" then showHome(): return true if m.sportsFocusArea = "tabs" then if key = "up" then m.sportsTabIndex = m.sportsTabIndex - 1 if m.sportsTabIndex < 0 then m.sportsTabIndex = 2 applySportsFilter() renderSportsScreen() return true else if key = "down" then m.sportsTabIndex = m.sportsTabIndex + 1 if m.sportsTabIndex > 2 then m.sportsTabIndex = 0 applySportsFilter() renderSportsScreen() return true else if key = "right" or key = "OK" then m.sportsFocusArea = "cards" m.sportsIndex = 0 m.sportsPage = 0 renderSportsScreen() return true end if return true end if total = 0 if m.sportsItems <> invalid then total = m.sportsItems.Count() if total = 0 then if key = "left" then m.sportsFocusArea = "tabs" renderSportsScreen() end if return true end if cols = 2 perPage = m.sportsPerPage if perPage <= 0 then perPage = 6 absIndex = (m.sportsPage * perPage) + m.sportsIndex if absIndex < 0 then absIndex = 0 if absIndex >= total then absIndex = total - 1 pageStart = m.sportsPage * perPage pageEnd = pageStart + perPage - 1 if pageEnd >= total then pageEnd = total - 1 localIndex = absIndex - pageStart col = localIndex mod cols newAbsIndex = absIndex if key = "OK" then selectSportsEvent() return true else if key = "left" then if col = 0 then m.sportsFocusArea = "tabs" renderSportsScreen() return true else newAbsIndex = absIndex - 1 end if else if key = "right" then if col = 0 and absIndex + 1 <= pageEnd then newAbsIndex = absIndex + 1 else newAbsIndex = absIndex end if else if key = "up" then if absIndex - cols >= pageStart then newAbsIndex = absIndex - cols else if pageStart > 0 then prevPageStart = pageStart - perPage prevPageEnd = pageStart - 1 targetRowStart = prevPageStart + perPage - cols newAbsIndex = targetRowStart + col if newAbsIndex > prevPageEnd then newAbsIndex = prevPageEnd if newAbsIndex < prevPageStart then newAbsIndex = prevPageStart else newAbsIndex = absIndex end if else if key = "down" then if absIndex + cols <= pageEnd then newAbsIndex = absIndex + cols else if pageEnd < total - 1 then nextPageStart = pageEnd + 1 nextPageEnd = nextPageStart + perPage - 1 if nextPageEnd >= total then nextPageEnd = total - 1 newAbsIndex = nextPageStart + col if newAbsIndex > nextPageEnd then newAbsIndex = nextPageEnd else newAbsIndex = absIndex end if else return true end if if newAbsIndex < 0 then newAbsIndex = 0 if newAbsIndex >= total then newAbsIndex = total - 1 m.sportsPage = Int(newAbsIndex / perPage) m.sportsIndex = newAbsIndex - (m.sportsPage * perPage) renderSportsScreen() return true end function sub selectSportsEvent() item = currentSportsEvent() if item = invalid then return title = sportsAny(item, ["title", "name", "nome", "competition", "evento"]) homeName = sportsAny(item, ["team1", "time_casa", "home", "mandante", "lutador1", "fighter1", "atleta1", "competidor1", "nome1"]) awayName = sportsAny(item, ["team2", "time_fora", "away", "visitante", "lutador2", "fighter2", "atleta2", "competidor2", "nome2"]) if homeName <> "" or awayName <> "" then title = homeName + " x " + awayName if title = "" then title = "Transmissão esportiva" directUrl = sportsDirectUrl(item) sid = sportsStreamId(item) if directUrl <> "" or sid <> "" then quickItem = { name:title, title:title, stream_url:directUrl, stream_id:sid } m.pendingSportsEvent = item playSportsFastItem(quickItem, title) return end if channel = sportsChannelName(item) if channel = "" then if m.sportsMessage <> invalid then m.sportsMessage.text = "Este evento não possui canal informado pela API." return end if cachedChannel = sportsFindChannelInCatalog(channel) if cachedChannel <> invalid then m.pendingSportsEvent = item playSportsFastItem(cachedChannel, title) return end if if m.sportsChannelResolverTask = invalid then if m.sportsMessage <> invalid then m.sportsMessage.text = "Resolvedor de canal não encontrado." return end if if m.sportsCatalogPrefetching = true then m.sportsChannelResolverTask.control = "STOP" m.sportsCatalogPrefetching = false end if m.pendingSportsEvent = item m.mode = "loading_sports_channel" if m.sportsMessage <> invalid then m.sportsMessage.text = "Procurando " + channel + " na sua lista..." m.sportsChannelResolverTask.control = "STOP" m.sportsChannelResolverTask.apiBase = m.apiBase m.sportsChannelResolverTask.token = m.token m.sportsChannelResolverTask.channelName = channel m.sportsChannelResolverTask.returnCatalog = false m.sportsChannelResolverTask.control = "RUN" end sub sub onSportsChannelResolverResult() res = m.sportsChannelResolverTask.result if m.sportsCatalogPrefetching = true and m.mode <> "loading_sports_channel" then m.sportsCatalogPrefetching = false if res <> invalid and res.success = true and res.items <> invalid and type(res.items) = "roArray" then m.sportsChannelCatalog = res.items end if return end if if m.mode <> "loading_sports_channel" then return if res = invalid or res.success <> true or res.item = invalid then m.mode = "sports" msg = "Canal não encontrado na lista." if res <> invalid and res.message <> invalid then msg = res.message.ToStr() renderSportsScreen() if m.sportsMessage <> invalid then m.sportsMessage.text = msg return end if channelItem = res.item m.mode = "sports" playSportsFastItem(channelItem, itemTitle(channelItem)) end sub sub showListEpgForItem(item as Dynamic) if m.epgBox = invalid then return end if if m.mode <> "live_streams" and m.mode <> "favorites" then hideListEpg() return end if nowText = "" nextText = "" if item <> invalid then if item.epg_now <> invalid then nowText = item.epg_now if item.now_title <> invalid then nowText = item.now_title if item.epg_next <> invalid then nextText = item.epg_next if item.next_title <> invalid then nextText = item.next_title end if m.epgBox.visible = true m.epgNow.visible = true m.epgNext.visible = true if nowText = "" and item <> invalid then m.epgNow.text = "Agora: carregando EPG..." m.epgNext.text = "Depois: aguarde..." loadEpgForItem(item) else if nowText = "" then nowText = "sem programação atual" if nextText = "" then nextText = "sem informação" m.epgNow.text = "Agora: " + nowText m.epgNext.text = "Depois: " + nextText end if end sub sub hideListEpg() if m.epgBox <> invalid then m.epgBox.visible = false end if if m.epgNow <> invalid then m.epgNow.visible = false end if if m.epgNext <> invalid then m.epgNext.visible = false end if end sub sub loadEpgForItem(item as Dynamic) sid = streamIdOf(item) if sid = "" then return m.currentEpgStreamId = sid url = m.apiBase + "/api/roku/epg.php?token=" + m.token + "&stream_id=" + sid m.epgTask.url = url if m.epgTimeoutTimer <> invalid then m.epgTimeoutTimer.control = "stop" m.epgTimeoutTimer.control = "start" end if m.epgTask.control = "RUN" end sub sub onEpgResult() if m.epgTimeoutTimer <> invalid then m.epgTimeoutTimer.control = "stop" res = m.epgTask.result if res = invalid or res.success <> true then m.currentPlayerEpg = "Agora: sem informação | Depois: sem informação" if m.mode = "player" and isLivePlayer() and m.playerEpg <> invalid then m.playerEpg.text = m.currentPlayerEpg m.playerEpg.visible = true if m.playerOverlay <> invalid then m.playerOverlay.visible = true if m.playerTitle <> invalid then m.playerTitle.visible = true if m.playerHint <> invalid then m.playerHint.visible = true end if if m.epgNow <> invalid then m.epgNow.text = "Agora: sem informação" if m.epgNext <> invalid then m.epgNext.text = "Depois: sem informação" if m.channelPreviewNow <> invalid then m.channelPreviewNow.text = "Agora: sem informação" if m.channelPreviewNext <> invalid then m.channelPreviewNext.text = "Depois: sem informação" return end if data = res.data if data = invalid then m.currentPlayerEpg = "Agora: sem informação | Depois: sem informação" else if data.success <> invalid and data.success <> true then m.currentPlayerEpg = "Agora: sem informação | Depois: sem informação" else m.currentPlayerEpg = epgTextFromData(data) if m.currentPlayerEpg = "" then m.currentPlayerEpg = "Agora: sem informação | Depois: sem informação" end if ' Atualiza lista/categorias quando estiver navegando em canais if m.mode = "live_streams" or m.mode = "favorites" then parts = m.currentPlayerEpg if m.epgNow <> invalid then m.epgNow.text = parts if m.epgNext <> invalid then m.epgNext.text = "" setChannelPreviewCombinedEpg(parts) end if ' Atualiza barra embaixo enquanto o canal já está aberto if m.mode = "player" and isLivePlayer() and m.playerEpg <> invalid then m.playerEpg.text = m.currentPlayerEpg m.playerEpg.visible = true if m.playerOverlay <> invalid then m.playerOverlay.visible = true if m.playerTitle <> invalid then m.playerTitle.visible = true if m.playerHint <> invalid then m.playerHint.visible = true if m.overlayTimer <> invalid then m.overlayTimer.control = "stop" m.overlayTimer.control = "start" end if end if end sub function isLivePlayer() as Boolean if m.returnModeAfterPlayer = invalid then return false if m.returnModeAfterPlayer = "live_streams" then return true if m.returnModeAfterPlayer = "favorites" then return true return false end function function epgValueToText(v as Dynamic) as String if v = invalid then return "" tv = type(v) if tv = "roString" or tv = "String" then return v.ToStr() if tv = "roInt" or tv = "Integer" or tv = "roFloat" or tv = "Float" then return v.ToStr() if tv = "roAssociativeArray" then if v.title <> invalid then return v.title.ToStr() if v.name <> invalid then return v.name.ToStr() if v.program <> invalid then return v.program.ToStr() if v.programme <> invalid then return v.programme.ToStr() if v.description <> invalid then return v.description.ToStr() end if return "" end function function epgTextFromList(list as Dynamic) as String if list = invalid or type(list) <> "roArray" or list.Count() = 0 then return "" end if nowText = "" nextText = "" if list.Count() > 0 then nowText = epgValueToText(list[0]) if list.Count() > 1 then nextText = epgValueToText(list[1]) if nowText = "" then nowText = "sem informação" if nextText = "" then nextText = "sem informação" return "Agora: " + nowText + " | Depois: " + nextText end function function epgTextFromData(data as Dynamic) as String if data = invalid then return "" ' Alguns painéis retornam { success:true, data:{...} } if data.data <> invalid and type(data.data) = "roAssociativeArray" then nested = epgTextFromData(data.data) if nested <> "" then return nested end if ' XUI pode retornar listas em vários nomes if data.items <> invalid then txt = epgTextFromList(data.items) if txt <> "" then return txt end if if data.epg_list <> invalid then txt = epgTextFromList(data.epg_list) if txt <> "" then return txt end if if data.epg_listings <> invalid then txt = epgTextFromList(data.epg_listings) if txt <> "" then return txt end if if data.listings <> invalid then txt = epgTextFromList(data.listings) if txt <> "" then return txt end if nowText = "" nextText = "" if data.now <> invalid then nowText = epgValueToText(data.now) if data.current <> invalid and nowText = "" then nowText = epgValueToText(data.current) if data.epg_now <> invalid and nowText = "" then nowText = epgValueToText(data.epg_now) if data.now_title <> invalid and nowText = "" then nowText = data.now_title.ToStr() if data.title <> invalid and nowText = "" then nowText = data.title.ToStr() if data.next <> invalid then nextText = epgValueToText(data.next) if data.epg_next <> invalid and nextText = "" then nextText = epgValueToText(data.epg_next) if data.next_title <> invalid and nextText = "" then nextText = data.next_title.ToStr() if nowText = "" and nextText = "" then return "" if nowText = "" then nowText = "sem informação" if nextText = "" then nextText = "sem informação" return "Agora: " + nowText + " | Depois: " + nextText end function function epgTextForPlayer(item as Dynamic) as String if item = invalid then return "" end if nowText = "" nextText = "" if item.epg_now <> invalid then nowText = item.epg_now if item.now_title <> invalid then nowText = item.now_title if item.epg_next <> invalid then nextText = item.epg_next if item.next_title <> invalid then nextText = item.next_title txt = "" if nowText <> "" then txt = "Agora: " + nowText if nextText <> "" then if txt <> "" then txt = txt + " | " txt = txt + "Depois: " + nextText end if return txt end function function awTwoDigits(n as Integer) as String s = n.ToStr() if n < 10 then s = "0" + s return s end function function awFormatPlayerTime(seconds as Dynamic) as String if seconds = invalid then seconds = 0 total = Int(seconds) if total < 0 then total = 0 h = Int(total / 3600) remainingSeconds = total - (h * 3600) mm = Int(remainingSeconds / 60) ss = remainingSeconds - (mm * 60) if h > 0 then return h.ToStr() + ":" + awTwoDigits(mm) + ":" + awTwoDigits(ss) end if return awTwoDigits(mm) + ":" + awTwoDigits(ss) end function sub resetMovieSimpleProgressUi(title as Dynamic) m.playerUiReady = false m.playerUiPosition = 0 m.playerUiDuration = 0 m.playerProgressHasValidSample = false if m.movieSimpleTitle <> invalid then safeTitle = "Reproduzindo" if title <> invalid then if title.ToStr() <> "" then safeTitle = awCutText(title.ToStr(), 75) end if m.movieSimpleTitle.text = safeTitle end if if m.movieSimpleTime <> invalid then m.movieSimpleTime.text = "00:00" if m.movieSimpleTotal <> invalid then m.movieSimpleTotal.text = "--:--" if m.movieSimpleBarFill <> invalid then m.movieSimpleBarFill.width = 10 end sub function awSafeVodProgress() as Object result = { position: 0, duration: 0, valid: false } if m.playerUiReady <> true then return result if m.video = invalid then return result rawPosition = 0 rawDuration = 0 if m.video.position <> invalid then rawPosition = Int(m.video.position) if m.video.duration <> invalid then rawDuration = Int(m.video.duration) if rawDuration <= 0 then if m.playerUiDuration > 0 and m.playerUiPosition >= 0 and m.playerUiPosition <= m.playerUiDuration then result.position = m.playerUiPosition result.duration = m.playerUiDuration result.valid = true end if return result end if if rawPosition < 0 then rawPosition = 0 ' position maior que duration é um valor antigo deixado pelo episódio anterior. ' Nunca usamos esse número para desenhar a barra, salvar progresso ou avançar. if rawPosition > rawDuration + 10 then if m.playerUiDuration > 0 and m.playerUiPosition >= 0 and m.playerUiPosition <= m.playerUiDuration then result.position = m.playerUiPosition result.duration = m.playerUiDuration result.valid = true end if return result end if if rawPosition > rawDuration then rawPosition = rawDuration ' Ao iniciar do começo, uma posição alta antes da primeira amostra válida ' só pode ser resíduo do conteúdo anterior. if m.playerProgressHasValidSample <> true and m.playerPendingSeekApplied = true then if m.playerPendingSeekSeconds <= 30 and rawPosition > 30 then return result end if m.playerUiPosition = rawPosition m.playerUiDuration = rawDuration m.playerProgressHasValidSample = true result.position = rawPosition result.duration = rawDuration result.valid = true return result end function sub awApplyPendingPlayerSeek() if m.playerPendingSeekApplied = true then return if m.playerPendingSeekSeconds = invalid or m.playerPendingSeekSeconds <= 30 then m.playerPendingSeekApplied = true return end if info = awSafeVodProgress() if info.valid <> true or info.duration <= 0 then return targetSeconds = Int(m.playerPendingSeekSeconds) ' Caso a fonte tenha trocado por uma versão menor, não retoma perto do final. if targetSeconds >= info.duration - 15 then targetSeconds = 0 if targetSeconds < 0 then targetSeconds = 0 m.video.seek = targetSeconds m.playerUiPosition = targetSeconds m.playerUiDuration = info.duration m.playerProgressHasValidSample = true m.playerPendingSeekApplied = true end sub sub onPlayerProgressTimer() if m.mode <> "player" then if m.playerProgressTimer <> invalid then m.playerProgressTimer.control = "stop" return end if if m.returnModeAfterPlayer <> "movie_detail" and isSeriesPlayerMode() = false then return awApplyPendingPlayerSeek() updateMovieSimpleProgress() end sub sub updateMovieSimpleProgress() currentSeconds = 0 totalSeconds = 0 progressInfo = awSafeVodProgress() if progressInfo.valid = true then currentSeconds = progressInfo.position totalSeconds = progressInfo.duration end if if m.movieSimpleTitle <> invalid then if m.pendingTitle <> invalid and m.pendingTitle <> "" then m.movieSimpleTitle.text = awCutText(m.pendingTitle, 75) else m.movieSimpleTitle.text = "Reproduzindo" end if end if if m.movieSimpleTime <> invalid then m.movieSimpleTime.text = awFormatPlayerTime(currentSeconds) if m.movieSimpleTotal <> invalid then if totalSeconds > 0 then m.movieSimpleTotal.text = awFormatPlayerTime(totalSeconds) else m.movieSimpleTotal.text = "--:--" end if end if if m.movieSimpleBarFill <> invalid then barWidth = 10 if totalSeconds > 0 then barWidth = Int((currentSeconds * 1140) / totalSeconds) if barWidth < 10 then barWidth = 10 if barWidth > 1140 then barWidth = 1140 m.movieSimpleBarFill.width = barWidth end if end sub sub setMovieSimpleBar(showBar as Boolean) if m.movieSimplePanel <> invalid then m.movieSimplePanel.visible = showBar if m.movieSimpleTitle <> invalid then m.movieSimpleTitle.visible = showBar if m.movieSimpleBarBg <> invalid then m.movieSimpleBarBg.visible = showBar if m.movieSimpleBarFill <> invalid then m.movieSimpleBarFill.visible = showBar if m.movieSimpleTime <> invalid then m.movieSimpleTime.visible = showBar if m.movieSimpleTotal <> invalid then m.movieSimpleTotal.visible = showBar if m.movieSimpleHint <> invalid then m.movieSimpleHint.visible = showBar end sub sub setNextEpisodePromptVisible(showPrompt as Boolean) if m.nextEpisodePanel <> invalid then m.nextEpisodePanel.visible = showPrompt if m.nextEpisodeSmall <> invalid then m.nextEpisodeSmall.visible = showPrompt if m.nextEpisodeTitle <> invalid then m.nextEpisodeTitle.visible = showPrompt if m.nextEpisodeHint <> invalid then m.nextEpisodeHint.visible = showPrompt if m.nextEpisodeButton <> invalid then m.nextEpisodeButton.visible = showPrompt if m.nextEpisodeButtonText <> invalid then m.nextEpisodeButtonText.visible = showPrompt m.nextEpisodePromptVisible = showPrompt end sub function getNextEpisodeForCurrent() as Dynamic if m.currentEpisode = invalid then return invalid currentId = episodeIdOf(m.currentEpisode) if currentId = "" then return invalid epList = invalid if m.seriesEpisodesBySeason <> invalid and m.currentSeason <> invalid then epList = m.seriesEpisodesBySeason[m.currentSeason] end if if epList = invalid and m.items <> invalid and m.items.Count() > 0 then epList = m.items end if if epList = invalid or epList.Count() = 0 then return invalid for i = 0 to epList.Count() - 1 ep = epList[i] if episodeIdOf(ep) = currentId then nextIndex = i + 1 if nextIndex < epList.Count() then return epList[nextIndex] end if end if end for return invalid end function sub showNextEpisodePrompt() if isSeriesPlayerMode() = false then return if m.nextEpisodeOffered = true and m.nextEpisodePromptVisible = true then return nextEp = getNextEpisodeForCurrent() if nextEp = invalid then return m.nextEpisodeItem = nextEp m.nextEpisodeOffered = true epTitle = episodeTitleOf(nextEp) if epTitle = invalid or epTitle = "" then epTitle = "Próximo episódio" if m.nextEpisodeTitle <> invalid then m.nextEpisodeTitle.text = awCutText("Próximo: " + epTitle, 42) end if if m.nextEpisodeSmall <> invalid then m.nextEpisodeSmall.text = "A seguir" end if if m.nextEpisodeButton <> invalid then m.nextEpisodeButton.color = m.nextEpisodeButtonColor end if if m.nextEpisodeButtonText <> invalid then btnTxt = m.nextEpisodeButtonLabel if btnTxt = invalid or btnTxt = "" then btnTxt = "PRÓXIMO EPISÓDIO" m.nextEpisodeButtonText.text = awCutText(UCase(btnTxt), 24) end if if m.nextEpisodeHint <> invalid then m.nextEpisodeHint.text = "OK = assistir | Voltar = continuar" end if setNextEpisodePromptVisible(true) if m.movieSimpleHideTimer <> invalid then m.movieSimpleHideTimer.control = "stop" end sub sub hideNextEpisodePrompt() setNextEpisodePromptVisible(false) end sub sub playNextEpisodeFromPrompt() if m.nextEpisodeItem = invalid then hideNextEpisodePrompt() return end if nextEp = m.nextEpisodeItem saveCurrentEpisodeProgress() if m.video <> invalid then m.video.control = "stop" m.video.visible = false end if setMovieSimpleBar(false) hideNextEpisodePrompt() m.nextEpisodeOffered = false m.nextEpisodeItem = invalid m.currentEpisode = nextEp m.returnModeAfterPlayer = "series_episodes" m.mode = "series_episodes" if m.items <> invalid then for i = 0 to m.items.Count() - 1 if episodeIdOf(m.items[i]) = episodeIdOf(nextEp) then m.page = Int(i / 4) m.menuIndex = i - (m.page * 4) exit for end if end for end if title = episodeTitleOf(nextEp) if title = invalid or title = "" then title = "Episódio" m.pendingTitle = title streamUrl = "" if nextEp.stream_url <> invalid and nextEp.stream_url <> "" then streamUrl = nextEp.stream_url.ToStr() if streamUrl = "" and nextEp.url <> invalid and nextEp.url <> "" then streamUrl = nextEp.url.ToStr() if streamUrl = "" and nextEp.play_url <> invalid and nextEp.play_url <> "" then streamUrl = nextEp.play_url.ToStr() if streamUrl <> "" then playVideo(title, awPreferDirectVodUrlForMkv(nextEp, streamUrl, "series"), awMediaFormatFromItem(nextEp, "mp4")) return end if epId = episodeIdOf(nextEp) if epId = "" then showNotice("EPISÓDIO", "Próximo episódio sem ID.") return end if m.mode = "loading_episode_play" clearAllTextForPlayer() m.apiGetTask.url = m.apiBase + "/api/roku/series-play.php?token=" + m.token + "&episode_id=" + epId m.apiGetTask.control = "RUN" end sub sub onNextEpisodeTimer() if m.mode <> "player" then return if isSeriesPlayerMode() = false then return if m.nextEpisodeEnabled <> true then return if m.nextEpisodeOffered = true then return if m.video = invalid then return progressInfo = awSafeVodProgress() if progressInfo.valid <> true then return currentSeconds = progressInfo.position totalSeconds = progressInfo.duration if totalSeconds <= 0 then return remainingSeconds = totalSeconds - currentSeconds limitSeconds = 60 if m.nextEpisodePromptSeconds <> invalid then limitSeconds = m.nextEpisodePromptSeconds if limitSeconds < 15 then limitSeconds = 60 if currentSeconds > 60 and remainingSeconds > 0 and remainingSeconds <= limitSeconds then showNextEpisodePrompt() end if end sub sub hideMovieSimpleBar() if m.nextEpisodePromptVisible = true then return setMovieSimpleBar(false) if m.mode = "player" then m.top.setFocus(true) end if end sub function isSeriesPlayerMode() as Boolean if m.returnModeAfterPlayer = invalid then return false if m.returnModeAfterPlayer = "series_episodes" then return true if m.returnModeAfterPlayer = "series_continue_list" then return true return false end function sub showMovieSimpleBar() if m.returnModeAfterPlayer = "movie_detail" or isSeriesPlayerMode() then m.top.setFocus(true) updateMovieSimpleProgress() if m.movieSimpleHint <> invalid then if m.returnModeAfterPlayer = "movie_detail" then m.movieSimpleHint.text = "OK = barra | Esquerda/Direita = voltar/avançar | Voltar = sair" else m.movieSimpleHint.text = "OK = barra | Esquerda/Direita = voltar/avançar | Voltar = sair" end if end if setMovieSimpleBar(true) if m.movieSimpleHideTimer <> invalid then m.movieSimpleHideTimer.control = "stop" m.movieSimpleHideTimer.control = "start" end if end if end sub function awCurrentSeekStepSeconds() as Integer if isSeriesPlayerMode() then s = m.playerSeekSeriesSeconds if s = invalid or s <= 0 then s = 30 if s > 60 then s = 60 return s end if s = m.playerSeekMovieSeconds if s = invalid or s <= 0 then s = 30 return s end function function awSeekLabel(seconds as Integer) as String if seconds = invalid then return "" absSeconds = Abs(seconds) if absSeconds >= 60 then mins = Int(absSeconds / 60) if mins <= 1 then return "1min" return mins.ToStr() + "min" end if return absSeconds.ToStr() + "s" end function sub movieSeek30(seekAmount as Integer) if m.returnModeAfterPlayer <> "movie_detail" and isSeriesPlayerMode() = false then return end if if m.video = invalid then return end if currentSeconds = 0 totalSeconds = 0 progressInfo = awSafeVodProgress() if progressInfo.valid <> true or progressInfo.duration <= 0 then showMovieSimpleBar() if m.movieSimpleHint <> invalid then m.movieSimpleHint.text = "Carregando o tempo do vídeo... aguarde um instante." return end if currentSeconds = progressInfo.position totalSeconds = progressInfo.duration targetSeconds = currentSeconds + seekAmount if targetSeconds < 0 then targetSeconds = 0 end if if totalSeconds > 0 then maxSeconds = totalSeconds - 5 if targetSeconds > maxSeconds then targetSeconds = maxSeconds end if end if if targetSeconds < 0 then targetSeconds = 0 end if m.video.seek = targetSeconds showMovieSimpleBar() updateMovieSimpleProgress() if m.movieSimpleHint <> invalid then label = awSeekLabel(seekAmount) if seekAmount > 0 then m.movieSimpleHint.text = "Avançou " + label + " | " + awFormatPlayerTime(targetSeconds) + " / " + awFormatPlayerTime(totalSeconds) else m.movieSimpleHint.text = "Voltou " + label + " | " + awFormatPlayerTime(targetSeconds) + " / " + awFormatPlayerTime(totalSeconds) end if end if end sub sub startHoldSeek(direction as Integer) if m.mode <> "player" then return if m.returnModeAfterPlayer <> "movie_detail" and isSeriesPlayerMode() = false then return baseStep = 30 if isSeriesPlayerMode() then baseStep = 60 m.holdSeekDirection = direction m.holdSeekStep = baseStep m.holdSeekTicks = 0 if m.holdSeekTimer <> invalid then m.holdSeekTimer.control = "stop" m.holdSeekTimer.control = "start" end if doHoldSeekStep() end sub sub stopHoldSeek() if m.holdSeekTimer <> invalid then m.holdSeekTimer.control = "stop" m.holdSeekDirection = 0 m.holdSeekStep = 0 m.holdSeekTicks = 0 if m.movieSimpleHideTimer <> invalid then m.movieSimpleHideTimer.control = "stop" m.movieSimpleHideTimer.control = "start" end if end sub sub onHoldSeekTimer() if m.mode <> "player" then stopHoldSeek() return end if if m.holdSeekDirection = invalid or m.holdSeekDirection = 0 then return doHoldSeekStep() end sub sub doHoldSeekStep() if m.holdSeekDirection = invalid or m.holdSeekDirection = 0 then return stepSeconds = m.holdSeekStep if stepSeconds = invalid or stepSeconds <= 0 then stepSeconds = 30 m.holdSeekTicks = m.holdSeekTicks + 1 ' Depois de alguns segundos segurando, acelera para navegar mais rápido. if m.holdSeekTicks >= 8 then stepSeconds = stepSeconds * 3 else if m.holdSeekTicks >= 4 then stepSeconds = stepSeconds * 2 end if if m.holdSeekDirection < 0 then stepSeconds = 0 - stepSeconds movieSeek30(stepSeconds) if m.movieSimpleHideTimer <> invalid then m.movieSimpleHideTimer.control = "stop" if m.movieSimpleHint <> invalid then totalSeconds = 0 currentSeconds = 0 if m.video <> invalid and m.video.position <> invalid then currentSeconds = Int(m.video.position) if m.video <> invalid and m.video.duration <> invalid then totalSeconds = Int(m.video.duration) if m.holdSeekDirection > 0 then m.movieSimpleHint.text = "Avançando segurando... | " + awFormatPlayerTime(currentSeconds) + " / " + awFormatPlayerTime(totalSeconds) else m.movieSimpleHint.text = "Voltando segurando... | " + awFormatPlayerTime(currentSeconds) + " / " + awFormatPlayerTime(totalSeconds) end if end if end sub sub toggleSimplePause() m.top.setFocus(true) pausedNow = false if m.video <> invalid then if m.video.state = "playing" then m.video.control = "pause" pausedNow = true else m.video.control = "resume" end if end if showMovieSimpleBar() if m.movieSimpleHint <> invalid then if pausedNow then m.movieSimpleHint.text = "Pausado | Play/Pause = continuar | Voltar = sair" else m.movieSimpleHint.text = "Reproduzindo | Play/Pause = pausar | Voltar = sair" end if end if end sub sub showPlayerOverlay(title as Dynamic) if title = invalid or title = "" then title = "Canal" end if if m.playerTitle <> invalid then m.playerTitle.text = title if m.playerOverlay <> invalid then m.playerOverlay.visible = true if m.playerTitle <> invalid then m.playerTitle.visible = true if m.playerHint <> invalid then if isLivePlayer() then m.playerHint.text = "OK = informações | Voltar = lista de canais" else m.playerHint.text = "OK = informações | Voltar = sair" end if m.playerHint.visible = true end if if m.playerEpg <> invalid then if isLivePlayer() then txt = m.currentPlayerEpg if txt = invalid or txt = "" then txt = "Agora: carregando programação..." m.playerEpg.text = txt m.playerEpg.visible = true else m.playerEpg.text = "" m.playerEpg.visible = false end if end if if m.overlayTimer <> invalid then m.overlayTimer.control = "stop" m.overlayTimer.control = "start" end if ' overlay_auto_hide_v144 if m.overlayTimer <> invalid then m.overlayTimer.control = "stop" m.overlayTimer.control = "start" end if end sub sub hidePlayerOverlay() if m.liveRecoveringVisible <> invalid then m.liveRecoveringVisible = false if m.expiryLabel <> invalid then m.expiryLabel.visible = false if m.expiryBg <> invalid then m.expiryBg.visible = false if m.playerOverlay <> invalid then m.playerOverlay.visible = false if m.playerTitle <> invalid then m.playerTitle.visible = false if m.playerHint <> invalid then m.playerHint.visible = false if m.playerEpg <> invalid then m.playerEpg.visible = false end sub sub clearAllTextForPlayer() if m.movieSimpleHideTimer <> invalid then m.movieSimpleHideTimer.control = "stop" setMovieSimpleBar(false) clearMovieUi() if m.epgTimeoutTimer <> invalid then m.epgTimeoutTimer.control = "stop" if m.epgHideTimer <> invalid then m.epgHideTimer.control = "stop" setPremiumHomeVisible(false) m.code.text = "" m.status.text = "" m.hint.text = "" m.footer.text = "" m.menuTitle.text = "" m.menu1.text = "" m.menu2.text = "" m.menu3.text = "" m.menu4.text = "" m.menu5.text = "" m.menu6.text = "" m.menu7.text = "" end sub function awCleanStreamUrlForExt(url as Dynamic) as String lowerUrl = "" if url <> invalid then lowerUrl = LCase(url.ToStr()) qPos = Instr(1, lowerUrl, "?") if qPos > 0 then lowerUrl = Left(lowerUrl, qPos - 1) hPos = Instr(1, lowerUrl, "#") if hPos > 0 then lowerUrl = Left(lowerUrl, hPos - 1) return lowerUrl end function function awMediaFormatFromItem(item as Dynamic, fallback as Dynamic) as String out = "" if fallback <> invalid then out = fallback.ToStr() if item = invalid then return out if item.container_extension <> invalid and item.container_extension <> "" then out = item.container_extension.ToStr() if item.stream_format <> invalid and item.stream_format <> "" then out = item.stream_format.ToStr() if item.extension <> invalid and item.extension <> "" then out = item.extension.ToStr() if item.ext <> invalid and item.ext <> "" then out = item.ext.ToStr() if item.info <> invalid then if item.info.container_extension <> invalid and item.info.container_extension <> "" then out = item.info.container_extension.ToStr() if item.info.stream_format <> invalid and item.info.stream_format <> "" then out = item.info.stream_format.ToStr() if item.info.extension <> invalid and item.info.extension <> "" then out = item.info.extension.ToStr() end if lowerOut = LCase(out) if lowerOut = "matroska" or Instr(1, lowerOut, "mkv") > 0 then return "mkv" if lowerOut = "mka" or lowerOut = "mks" then return "mkv" if lowerOut = "m4v" or lowerOut = "mov" then return "mp4" return out end function function awVodExtForUrl(ext as Dynamic) as String out = "mp4" if ext <> invalid and ext <> "" then out = ext.ToStr() out = LCase(out) if out = "auto" or out = "" then out = "mp4" if out = "matroska" then out = "mkv" if out = "mka" or out = "mks" then out = "mkv" if out = "hls" then out = "m3u8" return out end function function awBuildFastVodUrl(item as Dynamic, mediaKind as String) as String if item = invalid then return "" baseUrl = awReadRegistryValue("dns_base_url") userName = awReadRegistryValue("username") userPass = awReadRegistryValue("password") if baseUrl = "" and item.dns_base_url <> invalid then baseUrl = item.dns_base_url.ToStr() if baseUrl = "" and item.server_url <> invalid then baseUrl = item.server_url.ToStr() if userName = "" and item.username <> invalid then userName = item.username.ToStr() if userPass = "" and item.password <> invalid then userPass = item.password.ToStr() baseUrl = awTrimLiveBase(baseUrl) if baseUrl = "" or userName = "" or userPass = "" then return "" sid = "" if mediaKind = "series" then sid = episodeIdOf(item) else sid = movieIdOf(item) end if if sid = "" then return "" ext = awVodExtForUrl(awMediaFormatFromItem(item, "mp4")) if mediaKind = "series" then return baseUrl + "/series/" + userName + "/" + userPass + "/" + sid + "." + ext end if return baseUrl + "/movie/" + userName + "/" + userPass + "/" + sid + "." + ext end function function awPreferDirectVodUrlForMkv(item as Dynamic, streamUrl as Dynamic, mediaKind as String) as String url = "" if streamUrl <> invalid then url = streamUrl.ToStr() fmt = awMediaFormatFromItem(item, "") detected = awContentStreamFormat(url, fmt, false) ' Quando o item é MKV e a URL vem por rota intermediária/play.php sem extensão, ' alguns firmwares acabam caindo no demux.mp4. Montamos a rota Xtream direta, ' igual players IPTV costumam fazer: /movie/user/pass/id.mkv ou /series/user/pass/id.mkv. if detected = "mkv" then directUrl = awBuildFastVodUrl(item, mediaKind) if directUrl <> "" then return directUrl end if return url end function function awContentStreamFormat(url as Dynamic, fmt as Dynamic, liveFlag as Boolean) as String lowerFmt = "" lowerUrl = "" cleanUrl = awCleanStreamUrlForExt(url) if fmt <> invalid then lowerFmt = LCase(fmt.ToStr()) if url <> invalid then lowerUrl = LCase(url.ToStr()) if lowerFmt = "hls" or Instr(1, lowerUrl, ".m3u8") > 0 then return "hls" if lowerFmt = "dash" or Instr(1, lowerUrl, ".mpd") > 0 then return "dash" ' MKV/Matroska precisa ser identificado antes do fallback MP4. ' Algumas URLs chegam com token depois da extensão: filme.mkv?token=... if lowerFmt = "mkv" or lowerFmt = "matroska" or Instr(1, lowerFmt, "mkv") > 0 then return "mkv" if Right(cleanUrl, 4) = ".mkv" or Instr(1, lowerUrl, ".mkv?") > 0 or Instr(1, lowerUrl, ".mkv&") > 0 or Instr(1, lowerUrl, "%2emkv") > 0 then return "mkv" if lowerFmt = "mka" or Right(cleanUrl, 4) = ".mka" then return "mkv" if lowerFmt = "mks" or Right(cleanUrl, 4) = ".mks" then return "mkv" if Right(cleanUrl, 4) = ".mp4" then return "mp4" if Right(cleanUrl, 4) = ".m4v" then return "mp4" if Right(cleanUrl, 4) = ".mov" then return "mp4" if lowerFmt = "mp4" or lowerFmt = "m4v" or lowerFmt = "mov" then return "mp4" ' Para TV ao vivo Xtream/XUI na Roku, HLS é o caminho mais estável. if liveFlag then return "hls" return "mp4" end function function awPlaybackHeaders(url as Dynamic, liveFlag as Boolean) as Object headers = CreateObject("roAssociativeArray") ' ContentNode.HttpHeaders precisa receber um roAssociativeArray. ' A versão anterior enviava uma lista de strings e alguns firmwares ignoravam os headers. if liveFlag = false then headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" headers["Accept"] = "*/*" headers["Accept-Language"] = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7" headers["Accept-Encoding"] = "identity" headers["Connection"] = "keep-alive" end if return headers end function sub awApplyPlaybackHeaders(content as Object, url as Dynamic, liveFlag as Boolean) if content = invalid then return headers = awPlaybackHeaders(url, liveFlag) if headers <> invalid and headers.Count() > 0 then content.HttpHeaders = headers end if end sub sub awApplyMkvRetryContent(retryFormat as Dynamic, retryLabel as String, useHeaders as Boolean) if m.video = invalid then return if m.currentPlayUrl = invalid or m.currentPlayUrl = "" then return print "MKV retry mode="; retryLabel retryContent = CreateObject("roSGNode", "ContentNode") retryContent.url = m.currentPlayUrl retryContent.title = m.currentPlayTitle if retryContent.hasField("live") then retryContent.live = false if retryContent.hasField("contentType") then retryContent.contentType = "movie" if useHeaders then awApplyPlaybackHeaders(retryContent, m.currentPlayUrl, false) end if if retryFormat <> invalid and retryFormat <> "" then retryContent.streamformat = retryFormat if retryContent.hasField("streamFormat") then retryContent.streamFormat = retryFormat if retryContent.hasField("StreamFormat") then retryContent.StreamFormat = retryFormat end if m.video.control = "stop" m.video.content = retryContent awForceVideoAudioMax() m.video.visible = true m.video.control = "play" end sub sub awForceVideoAudioMax() if m.video = invalid then return ' Alguns modelos/versões de Roku não expõem controle de volume no Video. ' Por isso usamos hasField para não quebrar o app. if m.video.hasField("mute") then m.video.mute = false end if if m.video.hasField("muted") then m.video.muted = false end if if m.video.hasField("volume") then ' Máximo interno seguro. Não amplifica acima da fonte, mas evita volume interno baixo. m.video.volume = 1.0 end if end sub sub playVideo(title as String, url as String, fmt as String) if title = invalid or title = "" then title = "Episódio" end if if title = invalid or title = "" then title = "Episódio" end if m.pendingTitle = title m.liveRestartPending = false awStopLiveRestartDelayTimer() m.nextEpisodeOffered = false m.nextEpisodeItem = invalid hideNextEpisodePrompt() ' Limpa todos os textos do app para não ficar o "Aguarde..." por cima do vídeo. clearAllTextForPlayer() hideListEpg() ' Libera posição/duração e o conteúdo anterior antes de abrir outro episódio. if m.playerProgressTimer <> invalid then m.playerProgressTimer.control = "stop" m.playerPendingSeekSeconds = 0 m.playerPendingSeekApplied = true resetMovieSimpleProgressUi(title) m.video.control = "stop" m.video.content = invalid m.video.visible = true awForceVideoAudioMax() m.top.setFocus(true) ' Tenta desligar a UI nativa para ficar tela limpa. if m.video.hasField("enableUI") then m.video.enableUI = false if isLivePlayer() then m.currentLiveUrl = url m.currentLiveFmt = fmt m.currentLiveTitle = title end if content = CreateObject("roSGNode", "ContentNode") content.url = url content.title = title if content.hasField("live") then content.live = isLivePlayer() detectedFormat = awContentStreamFormat(url, fmt, isLivePlayer()) print "WEBTV VIDEO url="; url print "WEBTV VIDEO fmtIn="; fmt; " detected="; detectedFormat; " live="; isLivePlayer() m.currentPlayUrl = url m.currentPlayTitle = title m.currentPlayFmt = fmt m.currentPlayDetectedFormat = detectedFormat m.currentPlayMkvRetried = false m.currentPlayMkvRetryStep = 0 awApplyPlaybackHeaders(content, url, isLivePlayer()) if content.hasField("contentType") and isLivePlayer() = false then content.contentType = "movie" ' Correção MKV: nunca tentar MKV como MP4. ' O log demux.mp4/no playable tracks confirma que o fallback MP4 quebra o container Matroska. if detectedFormat <> "" then content.streamformat = detectedFormat if content.hasField("streamFormat") then content.streamFormat = detectedFormat if content.hasField("StreamFormat") then content.StreamFormat = detectedFormat end if m.video.content = content ' Só entra no modo player depois de liberar o conteúdo antigo. Assim, ' eventos finished/error atrasados do episódio anterior são ignorados. m.mode = "player" awForceVideoAudioMax() m.video.control = "play" awForceVideoAudioMax() if isLivePlayer() and m.liveOpenUsingFastUrl = true then if m.liveOpenTimer <> invalid then m.liveOpenTimer.control = "stop" m.liveOpenTimer.control = "start" end if else awStopLiveOpenTimer() end if if isSeriesPlayerMode() and m.nextEpisodeTimer <> invalid then m.nextEpisodeTimer.control = "stop" m.nextEpisodeTimer.control = "start" end if if m.progressSaveTimer <> invalid then m.progressSaveTimer.control = "stop" if m.returnModeAfterPlayer = "movie_detail" or isSeriesPlayerMode() then m.progressSaveTimer.control = "start" end if end if if m.playerProgressTimer <> invalid then m.playerProgressTimer.control = "stop" if m.returnModeAfterPlayer = "movie_detail" or isSeriesPlayerMode() then m.playerProgressTimer.control = "start" end if end if m.top.setFocus(true) forceStartFromBeginning = false if m.resumeForceStart <> invalid and m.resumeForceStart = true then forceStartFromBeginning = true if m.returnModeAfterPlayer = "movie_detail" then resumeSeconds = 0 if forceStartFromBeginning <> true then resumeSeconds = movieResumeSeconds(m.currentMovie) m.playerPendingSeekSeconds = resumeSeconds m.playerPendingSeekApplied = (resumeSeconds <= 30) m.top.setFocus(true) showMovieSimpleBar() if resumeSeconds > 30 and m.movieSimpleHint <> invalid then resumeMinutes = Int(resumeSeconds / 60) updateMovieSimpleProgress() m.movieSimpleHint.text = "Continuando de " + resumeMinutes.ToStr() + " min | OK = barra | Voltar = sair" else if forceStartFromBeginning = true and m.movieSimpleHint <> invalid then updateMovieSimpleProgress() m.movieSimpleHint.text = "Iniciando do começo | OK = barra | Voltar = sair" end if else if m.returnModeAfterPlayer = "series_episodes" or m.returnModeAfterPlayer = "series_continue_list" then epResume = 0 if forceStartFromBeginning <> true then epResume = episodeResumeSeconds(m.currentEpisode) m.playerPendingSeekSeconds = epResume m.playerPendingSeekApplied = (epResume <= 30) m.top.setFocus(true) showMovieSimpleBar() if epResume > 30 and m.movieSimpleHint <> invalid then epMinutes = Int(epResume / 60) updateMovieSimpleProgress() m.movieSimpleHint.text = "Continuando de " + epMinutes.ToStr() + " min | OK = barra | Voltar = sair" else if forceStartFromBeginning = true and m.movieSimpleHint <> invalid then updateMovieSimpleProgress() m.movieSimpleHint.text = "Iniciando do começo | OK = barra | Voltar = sair" end if else showPlayerOverlay(title) end if m.resumeForceStart = false end sub sub stopVideo() if m.liveRecoveringVisible <> invalid then m.liveRecoveringVisible = false awStopLiveOpenTimer() awStopLiveReconnectTimer() awStopLiveRestartDelayTimer() awStopLiveStableTimer() m.liveRestartPending = false m.liveOpenUsingFastUrl = false stopHoldSeek() if m.returnModeAfterPlayer = "movie_detail" then saveCurrentMovieProgress() else if m.returnModeAfterPlayer = "series_episodes" or m.returnModeAfterPlayer = "series_continue_list" then saveCurrentEpisodeProgress() end if if m.movieSimpleHideTimer <> invalid then m.movieSimpleHideTimer.control = "stop" end if if m.nextEpisodeTimer <> invalid then m.nextEpisodeTimer.control = "stop" end if if m.progressSaveTimer <> invalid then m.progressSaveTimer.control = "stop" end if if m.playerProgressTimer <> invalid then m.playerProgressTimer.control = "stop" end if m.playerUiReady = false m.playerProgressHasValidSample = false m.playerPendingSeekSeconds = 0 m.playerPendingSeekApplied = true hideNextEpisodePrompt() setMovieSimpleBar(false) hidePlayerOverlay() m.video.control = "stop" m.video.visible = false m.video.content = invalid m.top.setFocus(true) if m.returnModeAfterPlayer = "movie_detail" then m.mode = "movie_detail" showMovieDetail() else if m.returnModeAfterPlayer = "series_continue_list" then m.mode = "series_continue_list" m.items = getHomeSeriesContinueList() renderSeriesContinueList() else if m.returnModeAfterPlayer = "series_episodes" then m.mode = "series_episodes" if m.items = invalid or m.items.Count() = 0 then if m.seriesEpisodesBySeason <> invalid and m.currentSeason <> invalid then epsBack = m.seriesEpisodesBySeason[m.currentSeason] if epsBack <> invalid then m.items = epsBack end if end if end if renderSeriesEpisodes() else if m.returnModeAfterPlayer = "favorites" then m.mode = "favorites" m.channelFocusArea = "grid" renderList() else if m.returnModeAfterPlayer = "live_streams" then m.mode = "live_streams" m.channelFocusArea = "grid" if m.currentCategoryName = invalid then m.currentCategoryName = "Canais" end if renderList() else if m.returnModeAfterPlayer = "sports" then m.mode = "sports" showSportsScreenNodes(true) renderSportsScreen() else if m.currentEpisode <> invalid then m.returnModeAfterPlayer = "series_episodes" m.mode = "series_episodes" renderSeriesEpisodes() else showHome() end if end if end sub sub onVideoState() stateText = "" if m.video <> invalid and m.video.state <> invalid then stateText = m.video.state.ToStr() ' O Video pode emitir erro/finished depois do Voltar. Fora do player, ' esses eventos nunca podem abrir outra conexão. if m.mode <> "player" then return ' Ignora os últimos eventos da conexão que está sendo encerrada. if isLivePlayer() and m.liveRestartPending = true then return if stateText = "playing" then ' O canal iniciou: não precisamos mais consultar a rota alternativa. awStopLiveOpenTimer() awStopLiveReconnectTimer() m.liveOpenUsingFastUrl = false if isLivePlayer() = false then m.playerUiReady = true awApplyPendingPlayerSeek() updateMovieSimpleProgress() end if if isLivePlayer() then awHideLiveRecoveringIfNeeded() awStopLiveStableTimer() if m.liveStableTimer <> invalid then m.liveStableTimer.control = "start" else m.liveReconnectAttempts = 0 end if end if else if stateText = "buffering" then if isLivePlayer() then ' Se o buffer ficar preso por alguns segundos, tentamos religar o mesmo canal. awScheduleLiveReconnect() end if else if stateText = "finished" then if isLivePlayer() then ' Live não deveria finalizar. Quando a fonte corta, tenta recuperar sem voltar para a lista. awScheduleLiveReconnect() return end if if m.returnModeAfterPlayer = "movie_detail" then saveCurrentMovieProgress() stopVideo() else if m.returnModeAfterPlayer = "series_episodes" or m.returnModeAfterPlayer = "series_continue_list" then saveCurrentEpisodeProgress() if getNextEpisodeForCurrent() <> invalid then showNextEpisodePrompt() else stopVideo() end if else stopVideo() end if else if stateText = "error" then errCode = 0 errMsg = "" if m.video.errorCode <> invalid then errCode = m.video.errorCode if m.video.errorMsg <> invalid then errMsg = m.video.errorMsg print "LIVE/VIDEO ERROR code="; errCode; " message="; errMsg if m.video.errorStr <> invalid then print "LIVE/VIDEO ERROR str="; m.video.errorStr if m.video.errorInfo <> invalid then print "LIVE/VIDEO ERROR info="; m.video.errorInfo if isLivePlayer() then ' Primeiro tenta a rota resolvida pelo painel. Se ela também falhar, tenta religar o mesmo link. if m.liveFallbackRequested <> true and m.liveFallbackStreamId <> invalid and m.liveFallbackStreamId <> "" then awRequestResolvedLiveUrl() return end if awScheduleLiveReconnect() return end if ' Fallback MKV: nao tentar como MP4. Se for MKV, insistimos em Matroska. ' O erro demux.mp4/no playable tracks acontece justamente quando MKV é enviado como MP4. if m.currentPlayDetectedFormat = "mkv" and m.currentPlayUrl <> invalid and m.currentPlayUrl <> "" then if m.currentPlayMkvRetryStep = invalid then m.currentPlayMkvRetryStep = 0 if m.currentPlayMkvRetryStep = 0 then m.currentPlayMkvRetryStep = 1 awApplyMkvRetryContent("mkv", "mkv-without-custom-headers", false) return else if m.currentPlayMkvRetryStep = 1 then m.currentPlayMkvRetryStep = 2 awApplyMkvRetryContent("mkv", "mkv-with-browser-headers-again", true) return end if end if awStopLiveOpenTimer() awStopLiveReconnectTimer() hasNextEpisode = false if m.returnModeAfterPlayer = "series_episodes" or m.returnModeAfterPlayer = "series_continue_list" then saveCurrentEpisodeProgress() if getNextEpisodeForCurrent() <> invalid then hasNextEpisode = true end if stopVideo() if hasNextEpisode then showNotice("ERRO NO PLAYER", "A Roku nao conseguiu reproduzir este episodio. Aperte OK para tentar o proximo episodio.") showNextEpisodePrompt() if m.footer <> invalid then m.footer.text = "OK = proximo episodio | Voltar = retornar" else showNotice("ERRO NO PLAYER", "O link abriu, mas a Roku nao conseguiu reproduzir. Pode ser codec incompativel, formato MKV ou bloqueio da DNS.") end if end if end sub function streamIdOf(item as Dynamic) as String if item = invalid then return "" end if if item.stream_id <> invalid then return item.stream_id.ToStr() if item.id <> invalid then return item.id.ToStr() return "" end function function getFavorites() as Object sec = CreateObject("roRegistrySection", "awplayer") raw = "" if sec.Exists("favorites_live") then raw = sec.Read("favorites_live") if raw = invalid or raw = "" then return [] arr = ParseJson(raw) if arr = invalid or type(arr) <> "roArray" then return [] return arr end function sub saveFavorites(arr as Object) sec = CreateObject("roRegistrySection", "awplayer") sec.Write("favorites_live", FormatJson(arr)) sec.Flush() end sub function isFavorite(item as Dynamic) as Boolean sid = streamIdOf(item) if sid = "" then return false favs = getFavorites() for each f in favs if streamIdOf(f) = sid then return true end for return false end function sub updateLiveFavoritesCategoryCount() if m.channelCategories = invalid then return favCount = getFavorites().Count().ToStr() for each cat in m.channelCategories cid = "" if cat.category_id <> invalid then cid = cat.category_id.ToStr() if cid = "" and cat.id <> invalid then cid = cat.id.ToStr() if cid = "__favorites__" then cat.category_count = favCount cat.count = favCount exit for end if end for end sub function cloneLiveChannelForFavorite(item as Dynamic) as Object favItem = { stream_id: streamIdOf(item), id: streamIdOf(item), stream_display_name: itemTitle(item), name: itemTitle(item) } if item = invalid then return favItem if item.stream_url <> invalid then favItem.stream_url = item.stream_url if item.url <> invalid then favItem.url = item.url if item.play_url <> invalid then favItem.play_url = item.play_url if item.stream_icon <> invalid then favItem.stream_icon = item.stream_icon if item.logo <> invalid then favItem.logo = item.logo if item.image <> invalid then favItem.image = item.image if item.cover <> invalid then favItem.cover = item.cover if item.category_id <> invalid then favItem.category_id = item.category_id if item.container_extension <> invalid then favItem.container_extension = item.container_extension if item.epg_channel_id <> invalid then favItem.epg_channel_id = item.epg_channel_id if item.epg_now <> invalid then favItem.epg_now = item.epg_now if item.now_title <> invalid then favItem.now_title = item.now_title if item.epg_next <> invalid then favItem.epg_next = item.epg_next if item.next_title <> invalid then favItem.next_title = item.next_title return favItem end function sub toggleFavorite() if m.mode <> "live_streams" and m.mode <> "favorites" then return item = currentItem() if item = invalid then return sid = streamIdOf(item) if sid = "" then showNotice("FAVORITOS", "Este canal não possui ID para favoritar.") return end if favs = getFavorites() newFavs = [] removed = false for each f in favs if streamIdOf(f) = sid then removed = true else newFavs.Push(f) end if end for if removed = false then newFavs.Push(cloneLiveChannelForFavorite(item)) favoriteStatus = "Canal adicionado aos favoritos" else favoriteStatus = "Canal removido dos favoritos" end if saveFavorites(newFavs) updateLiveFavoritesCategoryCount() if m.mode = "favorites" then m.items = newFavs if m.items.count() = 0 then m.page = 0 m.menuIndex = 0 else if currentAbsIndex() >= m.items.count() then lastIndex = m.items.Count() - 1 m.page = Int(lastIndex / channelStreamsPageSize()) m.menuIndex = lastIndex - (m.page * channelStreamsPageSize()) end if end if renderList() if m.mode = "favorites" then m.footer.text = favoriteStatus + " | * = remover | Voltar = categorias" else m.footer.text = favoriteStatus + " | * = adicionar/remover | Voltar = categorias" end if end sub sub showFavorites() favs = getFavorites() m.mode = "favorites" m.items = favs m.menuIndex = 0 m.page = 0 m.channelCategoryIndex = 0 m.channelLoadedCategoryIndex = 0 m.channelFocusArea = "grid" m.currentCategoryName = "Favoritos" renderList() end sub sub clearSavedLoginAndShowLogin() ' Limpa token/sessão salva para forçar novo login por código/provedor. sec = CreateObject("roRegistrySection", "awplayer") if sec.Exists("token") then sec.Delete("token") if sec.Exists("activation_code") then sec.Delete("activation_code") if sec.Exists("device_code") then sec.Delete("device_code") if sec.Exists("username") then sec.Delete("username") if sec.Exists("password") then sec.Delete("password") if sec.Exists("dns_id") then sec.Delete("dns_id") if sec.Exists("provider_code") then sec.Delete("provider_code") sec.Flush() m.token = "" m.activationCode = invalid m.loginProviderCode = "" m.loginUsername = "" m.loginPassword = "" m.loginField = 0 m.loginDnsIndex = 0 m.loginStatusText = "" hideMovieLoading() clearHome() clearMovieUi() setPremiumHomeVisible(false) hideAllHomeRows() hideHomeBanner() clearMainMenuTexts() ' Recarrega configurações/DNS e mostra login. showLoginScreen() ' DNS/lista de provedores será carregada pela tela de login end sub sub refreshAppearanceFromAccount() ' Ação rápida e segura: não deixa a tela presa se o usuário apertar Voltar. if m.mode <> "account" then m.mode = "account" if m.footer <> invalid then m.footer.text = "Atualizando aparência... Aguarde" m.modeBeforeBannerConfig = "account" loadAppBanners() end sub sub showAccountMenu() ' Limpa qualquer tela moderna de canais/preview para não cobrir o menu da conta hideActivationText() hideMovieLoading() hideHomeBanner() hideStartupBanner() hideKeyboard() hideAllPosterOverlayBadges() hideAllHomeRows() clearMovieUi() setMovieSidebarVisible(false) setSeriesSidebarVisible(false) setChannelSidebarVisible(false) setChannelStreamUiVisible(false) hideListEpg() setPremiumHomeVisible(false) m.mode = "account" m.items = ["ATUALIZAR APARÊNCIA", "LOGIN / TROCAR USUÁRIO", "LIMPAR DADOS DO APP", "VOLTAR PARA HOME"] m.menuIndex = 0 m.page = 0 renderList() ' Fallback visual: garante que as opções apareçam mesmo após sair da tela de canais m.menuTitle.text = "CONTA" m.menu1.text = "> 001 ATUALIZAR APARÊNCIA" m.menu2.text = " 002 LOGIN / TROCAR USUÁRIO" m.menu3.text = " 003 LIMPAR DADOS DO APP" m.menu4.text = " 004 VOLTAR PARA HOME" m.menu5.text = "" m.menu6.text = "" m.menu7.text = "" m.footer.text = "OK = selecionar | Voltar = Home" end sub sub selectAccount() if m.menuIndex = 0 then ' ATUALIZAR APARÊNCIA refreshAppearanceFromAccount() else if m.menuIndex = 1 then ' LOGIN / TROCAR USUÁRIO clearSavedLoginAndShowLogin() else if m.menuIndex = 2 then ' LIMPAR ATIVAÇÃO clearSavedLoginAndShowLogin() else showHome() end if end sub function readToken() as String sec = CreateObject("roRegistrySection", "awplayer") if sec.Exists("token") then return sec.Read("token") end if return "" end function sub moveListUp() pageSize = channelStreamsPageSize() m.menuIndex = m.menuIndex - 1 if m.menuIndex < 0 then if m.page > 0 then m.page = m.page - 1 remain = m.items.count() - (m.page * pageSize) if remain > pageSize then remain = pageSize m.menuIndex = remain - 1 else m.page = Int((m.items.count() - 1) / pageSize) remain = m.items.count() - (m.page * pageSize) if remain > pageSize then remain = pageSize m.menuIndex = remain - 1 end if end if end sub sub moveListDown() pageSize = channelStreamsPageSize() m.menuIndex = m.menuIndex + 1 idx = currentChannelStreamAbsIndex() if m.menuIndex >= pageSize or idx >= m.items.count() then if ((m.page + 1) * pageSize) < m.items.count() then m.page = m.page + 1 m.menuIndex = 0 else m.page = 0 m.menuIndex = 0 end if end if end sub sub moveMovieGridLeft() m.menuIndex = m.menuIndex - 1 if m.menuIndex < 0 then if m.page > 0 then m.page = m.page - 1 remain = m.items.count() - (m.page * moviePerPage()) if remain > moviePerPage() then remain = moviePerPage() m.menuIndex = remain - 1 else m.page = Int((m.items.count() - 1) / moviePerPage()) remain = m.items.count() - (m.page * moviePerPage()) if remain > moviePerPage() then remain = moviePerPage() m.menuIndex = remain - 1 end if end if end sub sub moveMovieGridRight() m.menuIndex = m.menuIndex + 1 idx = currentAbsIndexMovie() if m.menuIndex >= moviePerPage() or idx >= m.items.count() then if ((m.page + 1) * moviePerPage()) < m.items.count() then m.page = m.page + 1 m.menuIndex = 0 else m.page = 0 m.menuIndex = 0 end if end if end sub sub seriesMoveList(moveAmount as Integer, perPage as Integer) if m.items = invalid then return end if totalItems = m.items.Count() if totalItems <= 0 then return end if absIndex = (m.page * perPage) + m.menuIndex absIndex = absIndex + moveAmount if absIndex < 0 then absIndex = totalItems - 1 end if if absIndex >= totalItems then absIndex = 0 end if m.page = Int(absIndex / perPage) m.menuIndex = absIndex - (m.page * perPage) end sub sub seriesMoveGrid(moveAmount as Integer, perPage as Integer) if m.items = invalid then return end if totalItems = m.items.Count() if totalItems <= 0 then return end if absIndex = (m.page * perPage) + m.menuIndex absIndex = absIndex + moveAmount if absIndex < 0 then absIndex = 0 end if if absIndex >= totalItems then absIndex = totalItems - 1 end if m.page = Int(absIndex / perPage) m.menuIndex = absIndex - (m.page * perPage) end sub function onKeyEvent(key as String, press as Boolean) as Boolean if press = false then return false end if ' PROMPT PREMIUM DE RETOMAR if m.mode = "resume_prompt" then if key = "left" or key = "right" then if m.resumePromptChoice = invalid then m.resumePromptChoice = 0 if m.resumePromptChoice = 0 then m.resumePromptChoice = 1 else m.resumePromptChoice = 0 end if renderResumePrompt() return true else if key = "OK" or key = "select" then startFromBeginning = (m.resumePromptChoice = 1) awStartResumePending(startFromBeginning) return true else if key = "back" then cancelResumePrompt() return true end if return true end if ' ATUALIZAÇÃO DE APARÊNCIA: bloqueia teclas durante a busca e Voltar retorna seguro. if m.mode = "banner_config" then if key = "back" then m.modeBeforeBannerConfig = invalid showAccountMenu() m.footer.text = "Atualização cancelada" return true end if return true end if ' LOGIN SCREEN if m.mode = "login" then if key = "up" then m.loginField = m.loginField - 1 if m.loginField < 0 then m.loginField = 4 renderLoginScreen() return true else if key = "down" then m.loginField = m.loginField + 1 if m.loginField > 4 then m.loginField = 0 renderLoginScreen() return true else if key = "left" or key = "right" then if m.loginField = 3 then m.loginField = 4 else if m.loginField = 4 then m.loginField = 3 end if renderLoginScreen() return true else if key = "OK" then if m.loginField = 0 then startLoginKeyboard("provider") else if m.loginField = 1 then startLoginKeyboard("username") else if m.loginField = 2 then startLoginKeyboard("password") else if m.loginField = 3 then loginSubmit() else if m.loginField = 4 then refreshLoginConfig() else renderLoginScreen() end if return true else if key = "back" then showLoginScreen() return true end if return true end if ' LOGIN DIALOG PADRÃO ROKU if m.mode = "login_dialog" then return false end if ' BUSCA COM TECLADO PADRÃO ROKU if m.mode = "search_dialog" then return false end if ' LOGIN KEYBOARD if m.mode = "login_keyboard" then if key = "back" then finishLoginKeyboard() return true end if ' Deixa o DynamicCustomKeyboard tratar setas, OK, shift, delete, espaço e continuar. return false end if ' SÉRIES - CONTINUAR LISTA if m.mode = "series_continue_list" then if key = "left" then if m.menuIndex = 0 or m.menuIndex = 4 then loadSeriesCategories() else m.menuIndex = m.menuIndex - 1 renderSeriesContinueList() end if return true else if key = "right" then if m.items <> invalid then idxNext = currentSeriesContinueAbsIndex() + 1 if idxNext < m.items.Count() then m.menuIndex = m.menuIndex + 1 if m.menuIndex >= seriesContinuePerPage() then m.page = m.page + 1 m.menuIndex = 0 end if end if end if renderSeriesContinueList() return true else if key = "up" then if m.menuIndex >= 4 then m.menuIndex = m.menuIndex - 4 else if m.page > 0 then m.page = m.page - 1 m.menuIndex = m.menuIndex + 4 end if renderSeriesContinueList() return true else if key = "down" then if m.items <> invalid then idxDown = currentSeriesContinueAbsIndex() + 4 if idxDown < m.items.Count() then if m.menuIndex < 4 then m.menuIndex = m.menuIndex + 4 else if ((m.page + 1) * seriesContinuePerPage()) < m.items.Count() then m.page = m.page + 1 m.menuIndex = m.menuIndex - 4 end if end if end if renderSeriesContinueList() return true else if key = "OK" then playSeriesContinueItem() return true else if key = "options" then epFav = invalid idxFav = currentSeriesContinueAbsIndex() if m.items <> invalid and idxFav >= 0 and idxFav < m.items.Count() then epFav = m.items[idxFav] if epFav <> invalid then sFav = { series_id: epFav.series_id, id: epFav.series_id, name: seriesContinueSeriesNameOf(epFav), title: seriesContinueSeriesNameOf(epFav), cover: seriesContinueCoverOf(epFav), cover_big: seriesContinueCoverOf(epFav) } m.currentSeries = sFav toggleSeriesFavorite() m.mode = "series_continue_list" renderSeriesContinueList() end if return true else if key = "back" then loadSeriesCategories() return true end if return true end if ' CARREGAMENTO DE SÉRIES NA MESMA TELA if m.mode = "loading_series_inline" then if key = "back" or key = "left" then m.apiGetTask.control = "STOP" returnToSeriesCategoriesFromGrid() return true end if return true end if ' SÉRIES - CATEGORIAS MODERNAS if m.mode = "series_categories" then if key = "up" then moveSeriesCategoryMenu(-1) return true else if key = "down" then moveSeriesCategoryMenu(1) return true else if key = "right" or key = "OK" then selectSeriesCategoryModern() return true else if key = "back" or key = "left" then showHome() return true end if return true end if if m.mode = "series_grid_modern" then if m.seriesFocusArea = "categories" then if key = "up" then moveSeriesSidebarIntegrated(-1) return true else if key = "down" then moveSeriesSidebarIntegrated(1) return true else if key = "right" or key = "OK" then loadSelectedSeriesCategoryFromSidebar() return true else if key = "back" then showHome() return true end if return true end if if key = "left" then if (m.menuIndex mod 4) = 0 then m.seriesFocusArea = "categories" else moveSeriesGridLeft() end if renderSeriesGridModern() return true else if key = "right" then moveSeriesGridRight() renderSeriesGridModern() return true else if key = "up" then moveSeriesGridUp() renderSeriesGridModern() return true else if key = "down" then moveSeriesGridDown() renderSeriesGridModern() return true else if key = "OK" then m.currentSeries = currentSeriesGridItem() if m.currentSeries <> invalid then m.seriesCameFromModernGrid = true showSeriesDetail() end if return true else if key = "options" then toggleSeriesFavorite() return true else if key = "back" then m.seriesFocusArea = "categories" renderSeriesGridModern() return true end if return true end if if m.mode = "series_grid" then if key = "left" then if (m.menuIndex mod 3) = 0 then returnToSeriesCategoriesFromGrid() else seriesMoveGrid(-1, 6) renderSeriesGrid() end if return true else if key = "right" then seriesMoveGrid(1, 6) renderSeriesGrid() return true else if key = "up" then seriesMoveGrid(-3, 6) renderSeriesGrid() return true else if key = "down" then seriesMoveGrid(3, 6) renderSeriesGrid() return true else if key = "OK" then m.currentSeries = invalid showSeriesDetail() return true else if key = "options" then toggleSeriesFavorite() return true else if key = "back" then loadSeriesCategories() return true end if return true end if if m.mode = "series_detail" then if key = "left" then m.menuIndex = m.menuIndex - 1 if m.menuIndex < 0 then m.menuIndex = 2 renderSeriesPremiumButtons(m.menuIndex) return true else if key = "right" then m.menuIndex = m.menuIndex + 1 if m.menuIndex > 2 then m.menuIndex = 0 renderSeriesPremiumButtons(m.menuIndex) return true else if key = "OK" then if m.menuIndex = 2 then toggleSeriesFavorite() renderSeriesPremiumBase() renderSeriesPremiumButtons(m.menuIndex) else loadSeriesInfo() end if return true else if key = "options" then toggleSeriesFavorite() renderSeriesPremiumBase() renderSeriesPremiumButtons(m.menuIndex) return true else if key = "back" then restoreSeriesGridAfterPremium() return true end if return true end if if m.mode = "series_seasons" then if key = "left" then seriesMoveList(-1, 4) renderSeriesSeasons() return true else if key = "right" then seriesMoveList(1, 4) renderSeriesSeasons() return true else if key = "up" then showSeriesDetail() return true else if key = "down" then return true else if key = "OK" then selectSeriesSeason() return true else if key = "back" then showSeriesDetail() return true end if return true end if if m.mode = "series_episodes" then if key = "left" then seriesMoveList(-1, 4) renderSeriesEpisodes() return true else if key = "right" then seriesMoveList(1, 4) renderSeriesEpisodes() return true else if key = "up" then m.mode = "series_seasons" m.items = m.seriesSeasons m.page = 0 m.menuIndex = 0 renderSeriesSeasons() return true else if key = "down" then return true else if key = "OK" then playEpisode() return true else if key = "back" then m.mode = "series_seasons" m.items = m.seriesSeasons renderSeriesSeasons() return true end if return true end if ' PLAYER if m.mode = "player" then if m.nextEpisodePromptVisible = true then if key = "OK" or key = "select" then playNextEpisodeFromPrompt() return true else if key = "back" then hideNextEpisodePrompt() showMovieSimpleBar() return true end if end if if key = "back" then stopVideo() return true else if key = "OK" or key = "select" then if m.returnModeAfterPlayer = "movie_detail" or isSeriesPlayerMode() then showMovieSimpleBar() else if m.pendingTitle = invalid or m.pendingTitle = "" then m.pendingTitle = "Episódio" end if showPlayerOverlay(m.pendingTitle) end if return true else if key = "play" or key = "pause" or key = "playpause" then if m.returnModeAfterPlayer = "movie_detail" or isSeriesPlayerMode() then toggleSimplePause() return true end if else if key = "left" or key = "rewind" then if m.returnModeAfterPlayer = "movie_detail" or isSeriesPlayerMode() then movieSeek30(0 - awCurrentSeekStepSeconds()) return true end if else if key = "right" or key = "fastforward" then if m.returnModeAfterPlayer = "movie_detail" or isSeriesPlayerMode() then movieSeek30(awCurrentSeekStepSeconds()) return true end if end if return false end if ' ATIVAÇÃO if m.mode = "activation" then if key = "OK" then checkActivation() return true end if return false end if if m.mode = "loading_sports" then if key = "back" then if m.sportsApiTask <> invalid then m.sportsApiTask.control = "STOP" showHome() end if return true end if if m.mode = "loading_sports_channel" then if key = "back" then if m.sportsChannelResolverTask <> invalid then m.sportsChannelResolverTask.control = "STOP" m.mode = "sports" showSportsScreenNodes(true) renderSportsScreen() end if return true end if if m.mode = "sports" then return handleSportsKey(key) end if ' HOME PREMIUM PLAYSIM / AW if m.mode = "home" then if key = "left" then if m.menuIndex = 6 then m.menuIndex = 3 else m.menuIndex = m.menuIndex - 1 if m.menuIndex < 0 then m.menuIndex = 5 end if renderPremiumSelection() return true else if key = "right" then if m.menuIndex = 6 then m.menuIndex = 3 else m.menuIndex = m.menuIndex + 1 if m.menuIndex > 5 then m.menuIndex = 0 end if renderPremiumSelection() return true else if key = "down" then m.menuIndex = 6 renderPremiumSelection() return true else if key = "up" then if m.menuIndex = 6 then m.menuIndex = 3 renderPremiumSelection() return true else if key = "OK" then selectHome() return true end if return false end if ' LISTA / INFO if m.mode = "list_info" then if key = "up" or key = "left" then m.menuIndex = m.menuIndex - 1 if m.menuIndex < 0 then m.menuIndex = 2 renderListInfoSelection() return true else if key = "down" or key = "right" then m.menuIndex = m.menuIndex + 1 if m.menuIndex > 2 then m.menuIndex = 0 renderListInfoSelection() return true else if key = "OK" then selectListInfo() return true else if key = "back" then showHome() return true end if return true end if ' CONTA if m.mode = "account" then if key = "up" then m.menuIndex = m.menuIndex - 1 if m.menuIndex < 0 then m.menuIndex = m.items.count() - 1 renderList() return true else if key = "down" then m.menuIndex = m.menuIndex + 1 if m.menuIndex >= m.items.count() then m.menuIndex = 0 renderList() return true else if key = "OK" then selectAccount() return true else if key = "back" then showHome() return true end if return false end if ' BUSCA DE FILMES / SÉRIES if m.mode = "movie_search_keyboard" then if m.searchKeyboardStyle = "zplay" then if key = "back" then finishSearchZplayKeyboard(false) return true else if key = "options" then toggleMovieLiveSearchType() return true end if ' Deixa o teclado ZPLAY tratar setas, OK, delete, espaço e continuar. return false end if if key = "up" then moveSearchUp() renderSearchKeyboard() return true else if key = "down" then moveSearchDown() renderSearchKeyboard() return true else if key = "left" then moveSearchLeft() renderSearchKeyboard() return true else if key = "right" then moveSearchRight() renderSearchKeyboard() return true else if key = "OK" then applySearchKey() return true else if key = "back" then hideKeyboard() if m.searchContentType = "series" then returnToSeriesCategoriesFromGrid() else loadMovieCategories() end if return true end if return false end if ' CARREGAMENTO DE FILMES NA MESMA TELA if m.mode = "loading_movies_inline" then if key = "back" or key = "left" then m.apiMovieTask.control = "STOP" returnToMovieCategoriesFromGrid() return true end if return true end if ' CATEGORIAS DE FILMES if m.mode = "movie_categories" then if key = "up" then moveMovieCategoryMenu(-1) return true else if key = "down" then moveMovieCategoryMenu(1) return true else if key = "right" or key = "OK" then selectMovieCategory() return true else if key = "back" or key = "left" then showHome() return true end if return false end if ' GRADE DE FILMES - categorias e cartazes na mesma tela if m.mode = "movie_grid" then if m.movieFocusArea = "categories" then if key = "up" then moveMovieSidebar(-1) return true else if key = "down" then moveMovieSidebar(1) return true else if key = "right" or key = "OK" then loadSelectedMovieCategoryFromSidebar() return true else if key = "back" then showHome() return true end if return true end if if key = "left" then if (m.menuIndex mod 4) = 0 then m.movieFocusArea = "categories" else moveMovieGridLeft() end if renderMovieGrid() return true else if key = "right" then moveMovieGridRight() renderMovieGrid() return true else if key = "up" then if m.menuIndex >= 4 then m.menuIndex = m.menuIndex - 4 else if m.page > 0 then m.page = m.page - 1 remain = m.items.count() - (m.page * moviePerPage()) if remain > moviePerPage() then remain = moviePerPage() m.menuIndex = remain - 1 if m.menuIndex < 0 then m.menuIndex = 0 end if renderMovieGrid() return true else if key = "down" then idxDown = m.menuIndex + 4 remain = m.items.count() - (m.page * moviePerPage()) if remain > moviePerPage() then remain = moviePerPage() if idxDown < remain then m.menuIndex = idxDown else if ((m.page + 1) * moviePerPage()) < m.items.count() then m.page = m.page + 1 m.menuIndex = 0 end if renderMovieGrid() return true else if key = "OK" then m.currentMovie = invalid showMovieDetail() return true else if key = "options" then toggleMovieFavorite() return true else if key = "back" then m.movieFocusArea = "categories" renderMovieGrid() return true end if return false end if ' DETALHE DO FILME if m.mode = "movie_detail" then if key = "OK" then playMovie() return true else if key = "options" then toggleMovieFavorite() return true else if key = "back" then ' Volta para a grade moderna de filmes com categorias laterais, sem cair no layout antigo. m.mode = "movie_grid" m.movieFocusArea = "grid" setMovieDetailVisible(false) setPremiumSeriesDetailVisible(false) renderMovieGrid() return true end if return false end if ' CANAIS - CATEGORIAS MODERNAS if m.mode = "live_categories" then if key = "up" then moveChannelSidebarSelection(-1) return true else if key = "down" then moveChannelSidebarSelection(1) return true else if key = "right" or key = "OK" then openChannelCategoryFromSidebar() return true else if key = "back" then showHome() return true end if return true end if ' CANAIS AO VIVO E FAVORITOS - categorias e grade na mesma tela if m.mode = "live_streams" or m.mode = "favorites" then if m.channelFocusArea = "categories" then if key = "up" then moveChannelSidebarSelection(-1) return true else if key = "down" then moveChannelSidebarSelection(1) return true else if key = "right" or key = "OK" then openChannelCategoryFromSidebar() return true else if key = "back" then showHome() return true end if return true end if totalChannelItems = 0 if m.items <> invalid then totalChannelItems = m.items.Count() pageSizeChannels = channelStreamsPageSize() remainChannels = totalChannelItems - (m.page * pageSizeChannels) if remainChannels > pageSizeChannels then remainChannels = pageSizeChannels if remainChannels < 0 then remainChannels = 0 if key = "left" then if (m.menuIndex mod 4) = 0 then m.channelFocusArea = "categories" else m.menuIndex = m.menuIndex - 1 end if renderList() return true else if key = "right" then if (m.menuIndex mod 4) < 3 and (m.menuIndex + 1) < remainChannels then m.menuIndex = m.menuIndex + 1 renderList() end if return true else if key = "up" then if m.menuIndex >= 4 then m.menuIndex = m.menuIndex - 4 else if m.page > 0 then oldColumn = m.menuIndex mod 4 m.page = m.page - 1 prevRemain = totalChannelItems - (m.page * pageSizeChannels) if prevRemain > pageSizeChannels then prevRemain = pageSizeChannels m.menuIndex = 4 + oldColumn if m.menuIndex >= prevRemain then m.menuIndex = prevRemain - 1 if m.menuIndex < 0 then m.menuIndex = 0 end if renderList() return true else if key = "down" then downIndex = m.menuIndex + 4 if downIndex < remainChannels then m.menuIndex = downIndex else if ((m.page + 1) * pageSizeChannels) < totalChannelItems then oldColumn = m.menuIndex mod 4 m.page = m.page + 1 nextRemain = totalChannelItems - (m.page * pageSizeChannels) if nextRemain > pageSizeChannels then nextRemain = pageSizeChannels m.menuIndex = oldColumn if m.menuIndex >= nextRemain then m.menuIndex = nextRemain - 1 if m.menuIndex < 0 then m.menuIndex = 0 end if renderList() return true else if key = "OK" then selectStream() return true else if key = "options" then toggleFavorite() return true else if key = "back" then m.channelFocusArea = "categories" renderList() return true end if return true end if ' AVISOS if m.mode = "notice" then if m.nextEpisodePromptVisible = true and (key = "OK" or key = "select") then playNextEpisodeFromPrompt() return true end if if m.nextEpisodePromptVisible = true and key = "back" then hideNextEpisodePrompt() end if if key = "back" or key = "OK" then showHome() return true end if end if return false end function sub onOverlayTimer() hidePlayerOverlay() end sub