├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── images │ ├── phone │ │ └── deviceframes.png │ └── tv │ │ ├── foryou.png │ │ ├── player.png │ │ └── playlist.png └── workflows │ └── android.yml ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── icon.png ├── runConfigurations │ ├── BaselineProfile_Smartphone.xml │ ├── BaselineProfile_TV.xml │ └── M3UAndroid___benchmark_Pixel5Api31BenchmarkAndroidTest_.xml └── vcs.xml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── RULES.md ├── app ├── extension │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── m3u │ │ │ └── extension │ │ │ ├── MainActivity.kt │ │ │ └── ui │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ └── res │ │ ├── mipmap-anydpi-v26 │ │ └── ic_launcher.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_monochrome.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_monochrome.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_monochrome.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_monochrome.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_monochrome.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml ├── smartphone │ ├── .gitignore │ ├── build.gradle.kts │ ├── icon.png │ ├── proguard-rules.pro │ └── src │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── generated │ │ │ └── baselineProfiles │ │ │ │ ├── baseline-prof.txt │ │ │ │ └── startup-prof.txt │ │ ├── ic_launcher-playstore.png │ │ ├── java │ │ │ └── com │ │ │ │ └── m3u │ │ │ │ └── smartphone │ │ │ │ ├── AppModule.kt │ │ │ │ ├── AppPublisher.kt │ │ │ │ ├── M3UApplication.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── TimeUtils.kt │ │ │ │ ├── glance │ │ │ │ ├── FavouriteWidget.kt │ │ │ │ └── GlanceReceiver.kt │ │ │ │ ├── startup │ │ │ │ └── ComposeInitializer.kt │ │ │ │ └── ui │ │ │ │ ├── App.kt │ │ │ │ ├── AppViewModel.kt │ │ │ │ ├── business │ │ │ │ ├── channel │ │ │ │ │ ├── ChannelMask.kt │ │ │ │ │ ├── ChannelMaskUtils.kt │ │ │ │ │ ├── ChannelScreen.kt │ │ │ │ │ ├── MaskGesture.kt │ │ │ │ │ ├── PlayerActivity.kt │ │ │ │ │ └── components │ │ │ │ │ │ ├── DlnaDeviceItem.kt │ │ │ │ │ │ ├── DlnaDevicesBottomSheet.kt │ │ │ │ │ │ ├── FormatItem.kt │ │ │ │ │ │ ├── FormatsBottomSheet.kt │ │ │ │ │ │ ├── MaskGestureValuePanel.kt │ │ │ │ │ │ ├── MaskValueButton.kt │ │ │ │ │ │ ├── PlayerMask.kt │ │ │ │ │ │ ├── PlayerPanel.kt │ │ │ │ │ │ ├── ProgrammeGuide.kt │ │ │ │ │ │ └── VerticalGestureArea.kt │ │ │ │ ├── configuration │ │ │ │ │ ├── PlaylistConfigurationNavigation.kt │ │ │ │ │ ├── PlaylistConfigurationScreen.kt │ │ │ │ │ └── components │ │ │ │ │ │ ├── AutoSyncProgrammesButton.kt │ │ │ │ │ │ ├── EpgManifestGallery.kt │ │ │ │ │ │ ├── SyncProgrammesButton.kt │ │ │ │ │ │ └── XtreamPanel.kt │ │ │ │ ├── crash │ │ │ │ │ ├── CrashActivity.kt │ │ │ │ │ ├── CrashApp.kt │ │ │ │ │ ├── CrashHandler.kt │ │ │ │ │ ├── components │ │ │ │ │ │ └── FileItem.kt │ │ │ │ │ ├── navigation │ │ │ │ │ │ └── Destination.kt │ │ │ │ │ └── screen │ │ │ │ │ │ ├── detail │ │ │ │ │ │ ├── DetailScreen.kt │ │ │ │ │ │ └── DetailViewModel.kt │ │ │ │ │ │ └── list │ │ │ │ │ │ ├── ListScreen.kt │ │ │ │ │ │ ├── ListViewModel.kt │ │ │ │ │ │ └── navigation │ │ │ │ │ │ └── ListNavigation.kt │ │ │ │ ├── extension │ │ │ │ │ └── ExtensionScreen.kt │ │ │ │ ├── favourite │ │ │ │ │ ├── FavouriteScreen.kt │ │ │ │ │ └── components │ │ │ │ │ │ ├── FavoriteGallery.kt │ │ │ │ │ │ └── FavoriteItem.kt │ │ │ │ ├── foryou │ │ │ │ │ ├── ForyouScreen.kt │ │ │ │ │ └── components │ │ │ │ │ │ ├── HeadlineBackground.kt │ │ │ │ │ │ ├── Loading.kt │ │ │ │ │ │ ├── PlaylistGallery.kt │ │ │ │ │ │ ├── PlaylistItem.kt │ │ │ │ │ │ └── recommend │ │ │ │ │ │ ├── RecommendGallery.kt │ │ │ │ │ │ └── RecommendItem.kt │ │ │ │ ├── playlist │ │ │ │ │ ├── PlaylistNavigation.kt │ │ │ │ │ ├── PlaylistScreen.kt │ │ │ │ │ └── components │ │ │ │ │ │ ├── ChannelGallery.kt │ │ │ │ │ │ ├── ChannelItem.kt │ │ │ │ │ │ └── PlaylistTabRow.kt │ │ │ │ └── setting │ │ │ │ │ ├── SettingScreen.kt │ │ │ │ │ ├── components │ │ │ │ │ ├── CanvasBottomSheet.kt │ │ │ │ │ ├── CheckBoxSharedPreference.kt │ │ │ │ │ ├── DataSourceSelection.kt │ │ │ │ │ ├── EpgPlaylistItem.kt │ │ │ │ │ ├── HiddenChannelItem.kt │ │ │ │ │ ├── HiddenPlaylistItem.kt │ │ │ │ │ ├── LocalStorageButton.kt │ │ │ │ │ ├── LocalStorageSwitch.kt │ │ │ │ │ └── RemoteControlSubscribeSwitch.kt │ │ │ │ │ └── fragments │ │ │ │ │ ├── AppearanceFragment.kt │ │ │ │ │ ├── OptionalFragment.kt │ │ │ │ │ ├── SubscriptionsFragment.kt │ │ │ │ │ └── preferences │ │ │ │ │ ├── OtherPreferences.kt │ │ │ │ │ ├── PreferencesFragment.kt │ │ │ │ │ └── RegularPreferences.kt │ │ │ │ ├── common │ │ │ │ ├── AppNavHost.kt │ │ │ │ ├── RootGraph.kt │ │ │ │ ├── Scaffold.kt │ │ │ │ ├── SmartphoneViewModel.kt │ │ │ │ ├── StarBackground.kt │ │ │ │ ├── helper │ │ │ │ │ ├── Element.kt │ │ │ │ │ ├── Helper.kt │ │ │ │ │ └── Metadata.kt │ │ │ │ └── internal │ │ │ │ │ ├── Events.kt │ │ │ │ │ ├── SmartphoneScaffoldImpl.kt │ │ │ │ │ ├── TabletScaffoldImpl.kt │ │ │ │ │ └── Toolkit.kt │ │ │ │ └── material │ │ │ │ ├── M3UHapticFeedback.kt │ │ │ │ ├── RecomposeHighlighter.kt │ │ │ │ ├── brush │ │ │ │ └── Scrim.kt │ │ │ │ ├── components │ │ │ │ ├── Backgrounds.kt │ │ │ │ ├── Badges.kt │ │ │ │ ├── BottomSheet.kt │ │ │ │ ├── Brushes.kt │ │ │ │ ├── Destination.kt │ │ │ │ ├── EpisodesBottomSheet.kt │ │ │ │ ├── EventHandler.kt │ │ │ │ ├── FontFamilies.kt │ │ │ │ ├── HorizontalPagerIndicator.kt │ │ │ │ ├── Images.kt │ │ │ │ ├── Layouts.kt │ │ │ │ ├── Lotties.kt │ │ │ │ ├── MediaSheet.kt │ │ │ │ ├── MonoText.kt │ │ │ │ ├── Player.kt │ │ │ │ ├── Preferences.kt │ │ │ │ ├── PullPanelLayout.kt │ │ │ │ ├── Selections.kt │ │ │ │ ├── SnackHost.kt │ │ │ │ ├── SortBottomSheet.kt │ │ │ │ ├── TextFields.kt │ │ │ │ ├── ThemeSelection.kt │ │ │ │ └── mask │ │ │ │ │ ├── Mask.kt │ │ │ │ │ ├── MaskButton.kt │ │ │ │ │ ├── MaskCircleButton.kt │ │ │ │ │ ├── MaskDefaults.kt │ │ │ │ │ ├── MaskInterceptor.kt │ │ │ │ │ ├── MaskPanel.kt │ │ │ │ │ └── MaskState.kt │ │ │ │ ├── effects │ │ │ │ └── BackStack.kt │ │ │ │ ├── ktx │ │ │ │ ├── Blurs.kt │ │ │ │ ├── Colors.kt │ │ │ │ ├── Effects.kt │ │ │ │ ├── Interaction.kt │ │ │ │ ├── InterceptEvent.kt │ │ │ │ ├── LifecycleEffect.kt │ │ │ │ ├── Modifier.kt │ │ │ │ ├── PaddingValues.kt │ │ │ │ ├── Pager.kt │ │ │ │ ├── Permissions.kt │ │ │ │ ├── ScrollableState.kt │ │ │ │ └── Unspecified.kt │ │ │ │ ├── model │ │ │ │ ├── Duration.kt │ │ │ │ ├── GradientColors.kt │ │ │ │ ├── LocalHazeState.kt │ │ │ │ ├── Spacing.kt │ │ │ │ └── Theme.kt │ │ │ │ ├── texture │ │ │ │ ├── TextureContainer.kt │ │ │ │ └── Textures.kt │ │ │ │ └── transformation │ │ │ │ ├── BlurTransformation.kt │ │ │ │ └── ColorCombineTransformation.kt │ │ └── res │ │ │ ├── drawable │ │ │ ├── baseline_history_toggle_off_24.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_launcher_foreground.xml │ │ │ ├── ic_splash.xml │ │ │ ├── round_calendar_month_24.xml │ │ │ └── round_space_dashboard_24.xml │ │ │ ├── font │ │ │ ├── google_sans.xml │ │ │ ├── jb_mono_medium.ttf │ │ │ ├── lexend_exa_medium.ttf │ │ │ ├── productsans_black.ttf │ │ │ ├── productsans_blackitalic.ttf │ │ │ ├── productsans_bold.ttf │ │ │ ├── productsans_bolditalic.ttf │ │ │ ├── productsans_italic.ttf │ │ │ ├── productsans_light.ttf │ │ │ ├── productsans_lightitalic.ttf │ │ │ ├── productsans_medium.ttf │ │ │ ├── productsans_mediumitalic.ttf │ │ │ ├── productsans_regular.ttf │ │ │ ├── productsans_thin.ttf │ │ │ └── productsans_thinitalic.ttf │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ │ ├── values-night │ │ │ └── colors.xml │ │ │ ├── values-v27 │ │ │ └── themes.xml │ │ │ ├── values-v29 │ │ │ └── themes.xml │ │ │ ├── values │ │ │ ├── colors.xml │ │ │ ├── stings.xml │ │ │ └── themes.xml │ │ │ ├── xml-v31 │ │ │ └── widget_info.xml │ │ │ └── xml │ │ │ ├── backup_rules.xml │ │ │ ├── data_extraction_rules.xml │ │ │ ├── filepaths.xml │ │ │ ├── shortcuts.xml │ │ │ └── widget_info.xml │ │ └── snapshotChannel │ │ └── res │ │ └── values │ │ ├── colors.xml │ │ └── stings.xml └── tv │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── m3u │ │ │ └── tv │ │ │ ├── App.kt │ │ │ ├── AppModule.kt │ │ │ ├── AppPublisher.kt │ │ │ ├── Colors.kt │ │ │ ├── Dialog.kt │ │ │ ├── M3UApplication.kt │ │ │ ├── MainActivity.kt │ │ │ ├── common │ │ │ ├── Error.kt │ │ │ ├── Loading.kt │ │ │ ├── MovieCard.kt │ │ │ ├── MoviesRow.kt │ │ │ └── PosterImage.kt │ │ │ ├── screens │ │ │ ├── Screens.kt │ │ │ ├── dashboard │ │ │ │ ├── DashboardScreen.kt │ │ │ │ ├── DashboardTopBar.kt │ │ │ │ ├── DashboardTopBarItemIndicator.kt │ │ │ │ └── UserAvatar.kt │ │ │ ├── favorite │ │ │ │ ├── FavoriteGallery.kt │ │ │ │ └── FavoriteScreen.kt │ │ │ ├── foryou │ │ │ │ ├── FeaturedMoviesCarousel.kt │ │ │ │ ├── ForyouScreen.kt │ │ │ │ └── Top10MoviesList.kt │ │ │ ├── player │ │ │ │ ├── ChannelScreen.kt │ │ │ │ └── components │ │ │ │ │ ├── RememberPlayer.kt │ │ │ │ │ ├── VideoPlayerControllerText.kt │ │ │ │ │ ├── VideoPlayerControls.kt │ │ │ │ │ ├── VideoPlayerControlsIcon.kt │ │ │ │ │ ├── VideoPlayerIndicator.kt │ │ │ │ │ ├── VideoPlayerMainFrame.kt │ │ │ │ │ ├── VideoPlayerMediaTitle.kt │ │ │ │ │ ├── VideoPlayerOverlay.kt │ │ │ │ │ ├── VideoPlayerPulse.kt │ │ │ │ │ ├── VideoPlayerSeeker.kt │ │ │ │ │ └── VideoPlayerState.kt │ │ │ ├── playlist │ │ │ │ ├── ChannelDetailScreen.kt │ │ │ │ ├── ChannelDetailViewModel.kt │ │ │ │ ├── ChannelDetails.kt │ │ │ │ ├── ChannelGallery.kt │ │ │ │ ├── DotSeparatedRow.kt │ │ │ │ ├── MovieReviews.kt │ │ │ │ ├── PlaylistScreen.kt │ │ │ │ └── TitleValueText.kt │ │ │ ├── profile │ │ │ │ ├── AboutSection.kt │ │ │ │ ├── AccountsSectionDeleteDialog.kt │ │ │ │ ├── AccountsSectionDialogButton.kt │ │ │ │ ├── AccountsSelectionItem.kt │ │ │ │ ├── HelpAndSupportSection.kt │ │ │ │ ├── LanguageSection.kt │ │ │ │ ├── ProfileScreen.kt │ │ │ │ ├── ProfileScreens.kt │ │ │ │ ├── SearchHistorySection.kt │ │ │ │ ├── SubscribeSection.kt │ │ │ │ └── SubtitlesSection.kt │ │ │ └── search │ │ │ │ ├── SearchScreen.kt │ │ │ │ └── SearchScreenViewModel.kt │ │ │ ├── startup │ │ │ └── ComposeInitializer.kt │ │ │ ├── theme │ │ │ ├── JetStreamFocusTheme.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ │ ├── ui │ │ │ ├── component │ │ │ │ └── TextFields.kt │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── utils │ │ │ ├── BringIntoViewIfChildrenAreFocused.kt │ │ │ ├── Colors.kt │ │ │ ├── GradientBg.kt │ │ │ ├── Helper.kt │ │ │ ├── ModifierUtils.kt │ │ │ └── Padding.kt │ └── res │ │ ├── font │ │ ├── inter_black.ttf │ │ ├── inter_bold.ttf │ │ ├── inter_extra_bold.ttf │ │ ├── inter_extra_light.ttf │ │ ├── inter_light.ttf │ │ ├── inter_medium.ttf │ │ ├── inter_regular.ttf │ │ ├── inter_semi_bold.ttf │ │ ├── inter_thin.ttf │ │ └── lexend_exa_medium.ttf │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── stings.xml │ │ └── themes.xml │ └── release │ └── generated │ └── baselineProfiles │ ├── baseline-prof.txt │ └── startup-prof.txt ├── baselineprofile ├── smartphone │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── m3u │ │ └── baselineprofile │ │ ├── BaselineProfileGenerator.kt │ │ └── StartupBenchmarks.kt └── tv │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── m3u │ └── baselineprofile │ ├── BaselineProfileGenerator.kt │ └── StartupBenchmarks.kt ├── build.gradle.kts ├── business ├── channel │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── m3u │ │ └── business │ │ └── channel │ │ ├── ChannelViewModel.kt │ │ └── PlayerState.kt ├── extension │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── m3u │ │ └── business │ │ └── extension │ │ └── ExtensionViewModel.kt ├── favorite │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── m3u │ │ │ └── business │ │ │ └── favorite │ │ │ └── FavouriteViewModel.kt │ │ └── res │ │ └── drawable │ │ └── round_play_arrow_24.xml ├── foryou │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── m3u │ │ │ └── business │ │ │ └── foryou │ │ │ ├── ForyouViewModel.kt │ │ │ └── Recommend.kt │ │ └── res │ │ └── raw │ │ ├── empty.json │ │ └── loading.json ├── playlist-configuration │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── m3u │ │ └── business │ │ └── playlist │ │ └── configuration │ │ ├── PlaylistConfigurationNavigation.kt │ │ └── PlaylistConfigurationViewModel.kt ├── playlist │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── m3u │ │ │ └── business │ │ │ └── playlist │ │ │ ├── PlaylistMessage.kt │ │ │ ├── PlaylistNavigation.kt │ │ │ └── PlaylistViewModel.kt │ │ └── res │ │ └── drawable │ │ └── round_play_arrow_24.xml └── setting │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── m3u │ │ └── business │ │ └── setting │ │ ├── BackingUpAndRestoringState.kt │ │ ├── SettingMessage.kt │ │ └── SettingViewModel.kt │ └── res │ └── drawable │ └── telegram.xml ├── compose_compiler_config.conf ├── core ├── .gitignore ├── README.md ├── build.gradle.kts ├── consumer-rules.pro ├── extension │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── m3u │ │ └── core │ │ └── extension │ │ ├── OnRemoteCall.kt │ │ ├── OnRemoteCallImpl.kt │ │ ├── RemoteService.kt │ │ ├── RemoteServiceDependencies.kt │ │ ├── RemoteServiceDependenciesImpl.kt │ │ ├── Utils.kt │ │ └── business │ │ ├── InfoModule.kt │ │ ├── RemoteModule.kt │ │ └── SubscribeModule.kt ├── foundation │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── m3u │ │ └── core │ │ └── foundation │ │ ├── components │ │ ├── AbsoluteSmoothCornerShape.kt │ │ ├── CircularProgressIndicator.kt │ │ └── SmoothCorner.kt │ │ ├── ktx │ │ └── NotNulls.kt │ │ ├── suggest │ │ └── Suggester.kt │ │ └── ui │ │ ├── Composable.kt │ │ ├── SugarColors.kt │ │ └── ThenIf.kt ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── m3u │ └── core │ ├── Contracts.kt │ ├── architecture │ ├── FileProvider.kt │ ├── Publisher.kt │ ├── Signal.kt │ ├── logger │ │ ├── Logger.kt │ │ ├── Profile.kt │ │ └── Profiles.kt │ └── preferences │ │ ├── ClipMode.kt │ │ ├── ConnectTimeout.kt │ │ ├── PlaylistStrategy.kt │ │ ├── Preferences.kt │ │ ├── ReconnectMode.kt │ │ └── UnseensMilliseconds.kt │ ├── unit │ └── DataUnit.kt │ ├── util │ ├── Files.kt │ ├── basic │ │ ├── Graphics.kt │ │ ├── LetIf.kt │ │ └── Strings.kt │ ├── collections │ │ ├── ForEachNotNull.kt │ │ └── IndexOf.kt │ ├── compose │ │ └── ObservableState.kt │ ├── context │ │ ├── Configuration.kt │ │ ├── SharedPreferences.kt │ │ └── Toasts.kt │ └── coroutine │ │ └── Flows.kt │ └── wrapper │ ├── Event.kt │ ├── Message.kt │ ├── Resource.kt │ └── Sort.kt ├── data ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro ├── schemas │ └── com.m3u.data.database.M3UDatabase │ │ ├── 1.json │ │ ├── 10.json │ │ ├── 11.json │ │ ├── 12.json │ │ ├── 13.json │ │ ├── 14.json │ │ ├── 15.json │ │ ├── 16.json │ │ ├── 17.json │ │ ├── 18.json │ │ ├── 19.json │ │ ├── 2.json │ │ ├── 20.json │ │ ├── 3.json │ │ ├── 4.json │ │ ├── 5.json │ │ ├── 6.json │ │ ├── 7.json │ │ ├── 8.json │ │ └── 9.json └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── m3u │ │ └── data │ │ ├── Certs.kt │ │ ├── SSLs.kt │ │ ├── api │ │ ├── ApiModule.kt │ │ ├── BaseUrls.kt │ │ └── dto │ │ │ └── github │ │ │ ├── Asset.kt │ │ │ ├── File.kt │ │ │ ├── Leaf.kt │ │ │ ├── Links.kt │ │ │ ├── Release.kt │ │ │ ├── Tree.kt │ │ │ └── User.kt │ │ ├── database │ │ ├── Converters.kt │ │ ├── DatabaseMigrations.kt │ │ ├── DatabaseModule.kt │ │ ├── M3UDatabase.kt │ │ ├── dao │ │ │ ├── ChannelDao.kt │ │ │ ├── ColorSchemeDao.kt │ │ │ ├── EpisodeDao.kt │ │ │ ├── PlaylistDao.kt │ │ │ └── ProgrammeDao.kt │ │ ├── example │ │ │ └── ColorSchemeExample.kt │ │ └── model │ │ │ ├── AdjacentChannels.kt │ │ │ ├── Channel.kt │ │ │ ├── ColorScheme.kt │ │ │ ├── Episode.kt │ │ │ ├── Playlist.kt │ │ │ └── Programme.kt │ │ ├── logger │ │ ├── MessageLogger.kt │ │ └── StubLogger.kt │ │ ├── model │ │ └── ChannelSet.kt │ │ ├── parser │ │ ├── ParserModule.kt │ │ ├── ParserUtils.kt │ │ ├── epg │ │ │ ├── EpgData.kt │ │ │ ├── EpgParser.kt │ │ │ └── EpgParserImpl.kt │ │ ├── m3u │ │ │ ├── M3UData.kt │ │ │ ├── M3UParser.kt │ │ │ └── M3UParserImpl.kt │ │ └── xtream │ │ │ ├── XtreamCategory.kt │ │ │ ├── XtreamChannelInfo.kt │ │ │ ├── XtreamData.kt │ │ │ ├── XtreamInfo.kt │ │ │ ├── XtreamOutput.kt │ │ │ ├── XtreamParser.kt │ │ │ └── XtreamParserImpl.kt │ │ ├── repository │ │ ├── BackupOrRestoreContracts.kt │ │ ├── CoroutineCache.kt │ │ ├── RepositoryModule.kt │ │ ├── channel │ │ │ ├── ChannelRepository.kt │ │ │ └── ChannelRepositoryImpl.kt │ │ ├── media │ │ │ ├── MediaRepository.kt │ │ │ └── MediaRepositoryImpl.kt │ │ ├── playlist │ │ │ ├── PlaylistRepository.kt │ │ │ └── PlaylistRepositoryImpl.kt │ │ └── programme │ │ │ ├── ProgrammeRepository.kt │ │ │ └── ProgrammeRepositoryImpl.kt │ │ ├── service │ │ ├── Messager.kt │ │ ├── PlayerManager.kt │ │ ├── ServicesModule.kt │ │ └── internal │ │ │ ├── ChannelPreferenceProvider.kt │ │ │ ├── Codecs.kt │ │ │ ├── ContinueWatchingCondition.kt │ │ │ ├── FileProviderImpl.kt │ │ │ ├── KodiAdaptions.kt │ │ │ ├── MessagerImpl.kt │ │ │ ├── PlayerManagerImpl.kt │ │ │ └── Utils.kt │ │ └── worker │ │ ├── BackupWorker.kt │ │ ├── ProgrammeReminder.kt │ │ ├── RestoreWorker.kt │ │ └── SubscriptionWorker.kt │ └── res │ └── drawable │ ├── baseline_notifications_none_24.xml │ ├── round_cancel_24.xml │ ├── round_file_download_24.xml │ └── round_refresh_24.xml ├── fastlane └── metadata │ └── android │ ├── en-US │ ├── full_description.txt │ ├── images │ │ ├── featureGraphic.png │ │ ├── icon.png │ │ ├── phoneScreenshots │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ └── 3.png │ │ └── tvScreenshots │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ └── 3.png │ ├── short_description.txt │ └── title.txt │ └── es-ES │ ├── full_description.txt │ └── short_description.txt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── i18n ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── res │ ├── values-de-rDE │ ├── app.xml │ ├── data.xml │ ├── feat_about.xml │ ├── feat_console.xml │ ├── feat_favourite.xml │ ├── feat_foryou.xml │ ├── feat_playlist.xml │ ├── feat_playlist_configuration.xml │ ├── feat_setting.xml │ ├── feat_stream.xml │ └── ui.xml │ ├── values-es-rES │ ├── app.xml │ ├── data.xml │ ├── feat_about.xml │ ├── feat_console.xml │ ├── feat_favourite.xml │ ├── feat_foryou.xml │ ├── feat_playlist.xml │ ├── feat_playlist_configuration.xml │ ├── feat_setting.xml │ ├── feat_stream.xml │ └── ui.xml │ ├── values-es-rMX │ ├── app.xml │ ├── data.xml │ ├── feat_about.xml │ ├── feat_console.xml │ ├── feat_favourite.xml │ ├── feat_foryou.xml │ ├── feat_playlist.xml │ ├── feat_playlist_configuration.xml │ ├── feat_setting.xml │ ├── feat_stream.xml │ └── ui.xml │ ├── values-id-rID │ ├── app.xml │ ├── data.xml │ ├── feat_about.xml │ ├── feat_console.xml │ ├── feat_favourite.xml │ ├── feat_foryou.xml │ ├── feat_playlist.xml │ ├── feat_playlist_configuration.xml │ ├── feat_setting.xml │ ├── feat_stream.xml │ └── ui.xml │ ├── values-pt-rBR │ ├── app.xml │ ├── data.xml │ ├── feat_about.xml │ ├── feat_console.xml │ ├── feat_favourite.xml │ ├── feat_foryou.xml │ ├── feat_playlist.xml │ ├── feat_playlist_configuration.xml │ ├── feat_setting.xml │ ├── feat_stream.xml │ └── ui.xml │ ├── values-ro-rRO │ ├── app.xml │ ├── data.xml │ ├── feat_about.xml │ ├── feat_console.xml │ ├── feat_favourite.xml │ ├── feat_foryou.xml │ ├── feat_playlist.xml │ ├── feat_setting.xml │ ├── feat_stream.xml │ └── ui.xml │ ├── values-tr-rTR │ ├── app.xml │ ├── data.xml │ ├── feat_about.xml │ ├── feat_console.xml │ ├── feat_favourite.xml │ ├── feat_foryou.xml │ ├── feat_playlist.xml │ ├── feat_playlist_configuration.xml │ ├── feat_setting.xml │ ├── feat_stream.xml │ └── ui.xml │ ├── values-zh-rCN │ ├── app.xml │ ├── data.xml │ ├── feat_about.xml │ ├── feat_console.xml │ ├── feat_favourite.xml │ ├── feat_foryou.xml │ ├── feat_playlist.xml │ ├── feat_playlist_configuration.xml │ ├── feat_setting.xml │ ├── feat_stream.xml │ └── ui.xml │ └── values │ ├── app.xml │ ├── data.xml │ ├── feat_about.xml │ ├── feat_console.xml │ ├── feat_favourite.xml │ ├── feat_foryou.xml │ ├── feat_playlist.xml │ ├── feat_playlist_configuration.xml │ ├── feat_setting.xml │ ├── feat_stream.xml │ └── ui.xml ├── jitpack.yml ├── lint.xml ├── lint ├── annotation │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── com │ │ └── m3u │ │ └── annotation │ │ ├── Exclude.kt │ │ ├── Likable.kt │ │ ├── Logger.kt │ │ └── MyDataClass.kt └── processor │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── m3u │ └── processor │ └── likable │ ├── LikableSymbolProcessor.kt │ ├── LikableSymbolProcessorProvider.kt │ ├── LoggerSymbolProcessor.kt │ ├── LoggerSymbolProcessorProvider.kt │ ├── MyDataClassSymbolProcessor.kt │ └── MyDataClassSymbolProcessorProvider.kt ├── play_store_512.png ├── renovate.json └── settings.gradle.kts /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Check before feedback** 11 | - [ ] I checked the other issues to ensure no one has mentioned it yet. 12 | - [ ] I made sure the bug can be reproduced in the [SNAPSHOT](https://nightly.link/oxyroid/M3UAndroid/workflows/android/master/artifact.zip) application package. 13 | 14 | **Describe the bug** 15 | A clear and concise description of what the bug is. 16 | 17 | **To Reproduce** 18 | Steps to reproduce the behavior: 19 | 20 | 1. Go to '...' 21 | 2. Click on '....' 22 | 3. Scroll down to '....' 23 | 4. See error 24 | 25 | **Expected behavior** 26 | A clear and concise description of what you expected to happen. 27 | 28 | **Screenshots** 29 | If applicable, add screenshots to help explain your problem. 30 | 31 | **Smartphone (please complete the following information):** 32 | 33 | - Device: [e.g. Google Pixel 4XL] 34 | - OS: [e.g. Android 12] 35 | - Version [e.g. 1.10.2] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/images/phone/deviceframes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/.github/images/phone/deviceframes.png -------------------------------------------------------------------------------- /.github/images/tv/foryou.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/.github/images/tv/foryou.png -------------------------------------------------------------------------------- /.github/images/tv/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/.github/images/tv/player.png -------------------------------------------------------------------------------- /.github/images/tv/playlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/.github/images/tv/playlist.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/deploymentTargetDropDown.xml 10 | /.idea/render.experimental.xml 11 | /.idea/kotlinc.xml 12 | /.idea/assetWizardSettings.xml 13 | .DS_Store 14 | /build 15 | /captures 16 | .externalNativeBuild 17 | .cxx 18 | local.properties 19 | jks.txt 20 | *.jks 21 | /sample/ 22 | /.kotlin 23 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | M3U -------------------------------------------------------------------------------- /.idea/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/.idea/icon.png -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/extension/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/extension/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -keep class com.squareup.wire.** { *; } 24 | -keep class com.m3u.extension.api.model.** { *; } 25 | -keep class * extends com.squareup.wire.ProtoAdapter -------------------------------------------------------------------------------- /app/extension/src/main/java/com/m3u/extension/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.extension.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/extension/src/main/java/com/m3u/extension/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.extension.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/extension/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/extension/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/extension/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/extension/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/extension/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/extension/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/extension/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/extension/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/extension/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/extension/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/extension/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/extension/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/extension/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/extension/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/extension/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/extension/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/extension/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/extension/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/extension/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/extension/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/extension/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/extension/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/extension/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | IPTV TxT 3 | -------------------------------------------------------------------------------- /app/extension/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | -------------------------------------------------------------------------------- /app/smartphone/src/main/res/values-v29/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /app/smartphone/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFBFE 4 | #1C1B1F 5 | #CEB951 6 | #61B34F 7 | -------------------------------------------------------------------------------- /app/smartphone/src/main/res/values/stings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | M3U 4 | -------------------------------------------------------------------------------- /app/smartphone/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 14 | -------------------------------------------------------------------------------- /app/smartphone/src/main/res/xml-v31/widget_info.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/smartphone/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/smartphone/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/smartphone/src/main/res/xml/filepaths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 15 | 18 | -------------------------------------------------------------------------------- /app/smartphone/src/main/res/xml/widget_info.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/smartphone/src/snapshotChannel/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #7F7FD5 4 | #91EAE4 5 | -------------------------------------------------------------------------------- /app/smartphone/src/snapshotChannel/res/values/stings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | M3U SNAPSHOT 4 | -------------------------------------------------------------------------------- /app/tv/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/tv/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -dontwarn reactor.blockhound.integration.BlockHoundIntegration -------------------------------------------------------------------------------- /app/tv/src/main/java/com/m3u/tv/AppModule.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.m3u.tv 4 | 5 | import com.m3u.core.architecture.Publisher 6 | import dagger.Binds 7 | import dagger.Module 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.components.SingletonComponent 10 | import javax.inject.Singleton 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | interface AppModule { 15 | @Binds 16 | @Singleton 17 | fun bindPublisher(provider: AppPublisher): Publisher 18 | } 19 | -------------------------------------------------------------------------------- /app/tv/src/main/java/com/m3u/tv/AppPublisher.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.tv 2 | 3 | import android.app.Application 4 | import android.os.Build 5 | import com.m3u.core.architecture.Abi 6 | import com.m3u.core.architecture.Publisher 7 | import javax.inject.Inject 8 | 9 | class AppPublisher @Inject constructor(private val application: Application) : Publisher { 10 | override val applicationId: String = BuildConfig.APPLICATION_ID 11 | override val versionName: String = BuildConfig.VERSION_NAME 12 | override val versionCode: Int = BuildConfig.VERSION_CODE 13 | override val debug: Boolean = BuildConfig.DEBUG 14 | override val model: String = Build.MODEL 15 | override val abi: Abi = Abi.of(Build.SUPPORTED_ABIS[0]) 16 | } -------------------------------------------------------------------------------- /app/tv/src/main/java/com/m3u/tv/M3UApplication.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.tv 2 | 3 | import android.app.Application 4 | import androidx.hilt.work.HiltWorkerFactory 5 | import androidx.work.Configuration 6 | import dagger.hilt.android.HiltAndroidApp 7 | import javax.inject.Inject 8 | 9 | @HiltAndroidApp 10 | class M3UApplication : Application(), Configuration.Provider { 11 | @Inject 12 | lateinit var workerFactory: HiltWorkerFactory 13 | 14 | override val workManagerConfiguration: Configuration by lazy { 15 | Configuration.Builder() 16 | .setWorkerFactory(workerFactory) 17 | .build() 18 | } 19 | 20 | override fun onCreate() { 21 | super.onCreate() 22 | } 23 | } -------------------------------------------------------------------------------- /app/tv/src/main/java/com/m3u/tv/common/Error.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.tv.common 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.tv.material3.Text 6 | 7 | @Composable 8 | fun Error(modifier: Modifier = Modifier) { 9 | Text( 10 | text = "error", 11 | modifier = modifier 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /app/tv/src/main/java/com/m3u/tv/common/Loading.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.tv.common 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Alignment 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.res.stringResource 8 | import androidx.compose.ui.text.TextStyle 9 | import androidx.tv.material3.MaterialTheme 10 | import androidx.tv.material3.Text 11 | 12 | @Composable 13 | fun Loading( 14 | modifier: Modifier = Modifier, 15 | style: TextStyle = MaterialTheme.typography.displayMedium 16 | ) { 17 | Box(modifier = modifier, contentAlignment = Alignment.Center) { 18 | Text( 19 | text = "Loading", 20 | style = style 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/tv/src/main/java/com/m3u/tv/common/PosterImage.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.tv.common 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.layout.ContentScale 6 | import androidx.compose.ui.platform.LocalContext 7 | import coil.compose.AsyncImage 8 | import coil.request.ImageRequest 9 | import com.m3u.data.database.model.Channel 10 | 11 | @Composable 12 | fun PosterImage( 13 | channel: Channel, 14 | modifier: Modifier = Modifier, 15 | ) { 16 | AsyncImage( 17 | modifier = modifier, 18 | model = ImageRequest.Builder(LocalContext.current) 19 | .crossfade(true) 20 | .data(channel.cover) 21 | .build(), 22 | contentDescription = channel.title, 23 | contentScale = ContentScale.Crop 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /app/tv/src/main/java/com/m3u/tv/screens/player/components/RememberPlayer.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.tv.screens.player.components 2 | 3 | import android.content.Context 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.remember 6 | import androidx.media3.common.C 7 | import androidx.media3.common.Player 8 | import androidx.media3.common.util.UnstableApi 9 | import androidx.media3.datasource.DefaultDataSource 10 | import androidx.media3.exoplayer.ExoPlayer 11 | import androidx.media3.exoplayer.source.ProgressiveMediaSource 12 | 13 | @androidx.annotation.OptIn(UnstableApi::class) 14 | @Composable 15 | fun rememberPlayer(context: Context) = remember { 16 | ExoPlayer.Builder(context) 17 | .setSeekForwardIncrementMs(10) 18 | .setSeekBackIncrementMs(10) 19 | .setMediaSourceFactory( 20 | ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context)) 21 | ) 22 | .setVideoScalingMode(C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING) 23 | .build() 24 | .apply { 25 | playWhenReady = true 26 | repeatMode = Player.REPEAT_MODE_ONE 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/tv/src/main/java/com/m3u/tv/screens/player/components/VideoPlayerControllerText.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.tv.screens.player.components 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.dp 8 | import androidx.tv.material3.MaterialTheme 9 | import androidx.tv.material3.Text 10 | 11 | @Composable 12 | fun VideoPlayerControllerText(text: String) { 13 | Text( 14 | modifier = Modifier.padding(horizontal = 12.dp), 15 | text = text, 16 | color = MaterialTheme.colorScheme.onSurface, 17 | fontWeight = FontWeight.SemiBold 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /app/tv/src/main/java/com/m3u/tv/screens/playlist/TitleValueText.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.tv.screens.playlist 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.draw.alpha 7 | import androidx.compose.ui.text.font.FontWeight 8 | import androidx.tv.material3.MaterialTheme 9 | import androidx.tv.material3.Text 10 | 11 | @Composable 12 | fun TitleValueText( 13 | modifier: Modifier = Modifier, 14 | title: String, 15 | value: String 16 | ) { 17 | Column(modifier = modifier) { 18 | Text( 19 | modifier = Modifier.alpha(0.75f), 20 | text = title, 21 | style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Normal) 22 | ) 23 | Text( 24 | text = value, 25 | style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Normal), 26 | maxLines = 3 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/tv/src/main/java/com/m3u/tv/screens/profile/ProfileScreens.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.tv.screens.profile 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.filled.MusicNote 6 | import androidx.compose.material.icons.filled.Support 7 | import androidx.compose.material.icons.filled.Translate 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import com.m3u.i18n.R 10 | 11 | enum class ProfileScreens( 12 | val icon: ImageVector, 13 | @StringRes val title: Int 14 | ) { 15 | Subscribe(Icons.Default.MusicNote, R.string.feat_setting_label_subscribe), 16 | // Appearance(Icons.Default.ColorLens, R.string.feat_setting_appearance), 17 | Optional(Icons.Default.Translate, R.string.feat_setting_optional_features), 18 | HelpAndSupport(Icons.Default.Support, R.string.feat_about_title); 19 | 20 | operator fun invoke() = name 21 | } 22 | -------------------------------------------------------------------------------- /app/tv/src/main/java/com/m3u/tv/startup/ComposeInitializer.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.tv.startup 2 | 3 | import android.content.Context 4 | import androidx.compose.ui.platform.ComposeView 5 | import androidx.lifecycle.ProcessLifecycleInitializer 6 | import androidx.startup.Initializer 7 | 8 | @Suppress("Unused") 9 | /** 10 | * warmup compose component 11 | * 12 | * https://medium.com/androiddevelopers/faster-jetpack-compose-view-interop-with-app-startup-and-baseline-profile-8a615e061d14 13 | */ 14 | class ComposeInitializer: Initializer { 15 | override fun create(context: Context) { 16 | ComposeView(context) 17 | } 18 | 19 | override fun dependencies(): List>> { 20 | return listOf(ProcessLifecycleInitializer::class.java) 21 | } 22 | } -------------------------------------------------------------------------------- /app/tv/src/main/java/com/m3u/tv/theme/JetStreamFocusTheme.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.tv.theme 2 | 3 | import androidx.compose.ui.unit.dp 4 | import androidx.tv.material3.ShapeDefaults 5 | 6 | val JetStreamCardShape = ShapeDefaults.ExtraSmall 7 | val JetStreamButtonShape = ShapeDefaults.ExtraSmall 8 | val IconSize = 20.dp 9 | val JetStreamBorderWidth = 3.dp 10 | 11 | /** 12 | * Space to be given below every Lazy (or scrollable) vertical list throughout the app 13 | */ 14 | val JetStreamBottomListPadding = 28.dp 15 | -------------------------------------------------------------------------------- /app/tv/src/main/java/com/m3u/tv/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.tv.theme 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.ui.graphics.toArgb 6 | import androidx.tv.material3.MaterialTheme 7 | import com.m3u.tv.createScheme 8 | import com.m3u.tv.utils.Indigo300 9 | 10 | @Composable 11 | fun JetStreamTheme( 12 | content: @Composable () -> Unit 13 | ) { 14 | MaterialTheme( 15 | colorScheme = 16 | // TODO: 17 | remember { createScheme(Indigo300.toArgb(), true) } 18 | , 19 | shapes = MaterialTheme.shapes, 20 | typography = Typography, 21 | content = content 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /app/tv/src/main/java/com/m3u/tv/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.tv.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/tv/src/main/java/com/m3u/tv/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.tv.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.runtime.Composable 5 | import androidx.tv.material3.ExperimentalTvMaterial3Api 6 | import androidx.tv.material3.MaterialTheme 7 | import androidx.tv.material3.darkColorScheme 8 | import androidx.tv.material3.lightColorScheme 9 | 10 | @OptIn(ExperimentalTvMaterial3Api::class) 11 | @Composable 12 | fun M3UTheme( 13 | isInDarkTheme: Boolean = isSystemInDarkTheme(), 14 | content: @Composable () -> Unit, 15 | ) { 16 | val colorScheme = if (isInDarkTheme) { 17 | darkColorScheme( 18 | primary = Purple80, 19 | secondary = PurpleGrey80, 20 | tertiary = Pink80 21 | ) 22 | } else { 23 | lightColorScheme( 24 | primary = Purple40, 25 | secondary = PurpleGrey40, 26 | tertiary = Pink40 27 | ) 28 | } 29 | MaterialTheme( 30 | colorScheme = colorScheme, 31 | typography = Typography, 32 | content = content 33 | ) 34 | } -------------------------------------------------------------------------------- /app/tv/src/main/java/com/m3u/tv/utils/GradientBg.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.tv.utils 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Brush 10 | import androidx.compose.ui.unit.dp 11 | 12 | val pairs = listOf( 13 | Coral to LightYellow, 14 | Red300 to BlueGray300, 15 | Pink300 to Gray300, 16 | Purple300 to Brown300, 17 | DeepPurple300 to DeepOrange300, 18 | Indigo300 to Orange300, 19 | Blue300 to Amber300, 20 | LightBlue300 to Yellow300, 21 | Cyan300 to Lime300, 22 | Teal300 to LightGreen300, 23 | Green300 to Coral, 24 | ) 25 | 26 | @Composable 27 | fun GradientBg() { 28 | Box( 29 | modifier = Modifier 30 | .background(Brush.radialGradient(pairs.random().toList())) 31 | .fillMaxWidth() 32 | .height(200.dp) 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /app/tv/src/main/java/com/m3u/tv/utils/Padding.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.tv.utils 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.unit.Dp 5 | 6 | @Immutable 7 | data class Padding( 8 | val start: Dp, 9 | val top: Dp, 10 | val end: Dp, 11 | val bottom: Dp, 12 | ) 13 | -------------------------------------------------------------------------------- /app/tv/src/main/res/font/inter_black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/tv/src/main/res/font/inter_black.ttf -------------------------------------------------------------------------------- /app/tv/src/main/res/font/inter_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/tv/src/main/res/font/inter_bold.ttf -------------------------------------------------------------------------------- /app/tv/src/main/res/font/inter_extra_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/tv/src/main/res/font/inter_extra_bold.ttf -------------------------------------------------------------------------------- /app/tv/src/main/res/font/inter_extra_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/tv/src/main/res/font/inter_extra_light.ttf -------------------------------------------------------------------------------- /app/tv/src/main/res/font/inter_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/tv/src/main/res/font/inter_light.ttf -------------------------------------------------------------------------------- /app/tv/src/main/res/font/inter_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/tv/src/main/res/font/inter_medium.ttf -------------------------------------------------------------------------------- /app/tv/src/main/res/font/inter_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/tv/src/main/res/font/inter_regular.ttf -------------------------------------------------------------------------------- /app/tv/src/main/res/font/inter_semi_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/tv/src/main/res/font/inter_semi_bold.ttf -------------------------------------------------------------------------------- /app/tv/src/main/res/font/inter_thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/tv/src/main/res/font/inter_thin.ttf -------------------------------------------------------------------------------- /app/tv/src/main/res/font/lexend_exa_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/tv/src/main/res/font/lexend_exa_medium.ttf -------------------------------------------------------------------------------- /app/tv/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/tv/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/tv/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/tv/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/tv/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/tv/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/tv/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/tv/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/tv/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/app/tv/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/tv/src/main/res/values/stings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | M3U 4 | -------------------------------------------------------------------------------- /app/tv/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /baselineprofile/smartphone/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /baselineprofile/smartphone/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /baselineprofile/smartphone/src/main/java/com/m3u/baselineprofile/BaselineProfileGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.baselineprofile 2 | 3 | import androidx.benchmark.macro.junit4.BaselineProfileRule 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import androidx.test.filters.LargeTest 6 | import androidx.test.platform.app.InstrumentationRegistry 7 | 8 | import org.junit.Rule 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | 12 | @RunWith(AndroidJUnit4::class) 13 | @LargeTest 14 | class BaselineProfileGenerator { 15 | 16 | @get:Rule 17 | val rule = BaselineProfileRule() 18 | 19 | @Test 20 | fun generate() { 21 | rule.collect( 22 | packageName = InstrumentationRegistry.getArguments().getString("targetAppId") 23 | ?: throw Exception("targetAppId not passed as instrumentation runner arg"), 24 | includeInStartupProfile = true 25 | ) { 26 | pressHome() 27 | startActivityAndWait() 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /baselineprofile/tv/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /baselineprofile/tv/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /baselineprofile/tv/src/main/java/com/m3u/baselineprofile/BaselineProfileGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.baselineprofile 2 | 3 | import androidx.benchmark.macro.junit4.BaselineProfileRule 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import androidx.test.filters.LargeTest 6 | import androidx.test.platform.app.InstrumentationRegistry 7 | 8 | import org.junit.Rule 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | 12 | @RunWith(AndroidJUnit4::class) 13 | @LargeTest 14 | class BaselineProfileGenerator { 15 | 16 | @get:Rule 17 | val rule = BaselineProfileRule() 18 | 19 | @Test 20 | fun generate() { 21 | rule.collect( 22 | packageName = InstrumentationRegistry.getArguments().getString("targetAppId") 23 | ?: throw Exception("targetAppId not passed as instrumentation runner arg"), 24 | includeInStartupProfile = true 25 | ) { 26 | pressHome() 27 | startActivityAndWait() 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /business/channel/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /src/test 3 | /src/androidTest -------------------------------------------------------------------------------- /business/channel/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/business/channel/consumer-rules.pro -------------------------------------------------------------------------------- /business/channel/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /business/channel/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /business/channel/src/main/java/com/m3u/business/channel/PlayerState.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.business.channel 2 | 3 | import android.graphics.Rect 4 | import androidx.compose.runtime.Immutable 5 | import androidx.media3.common.PlaybackException 6 | import androidx.media3.common.Player 7 | 8 | @Immutable 9 | data class PlayerState( 10 | val playState: @Player.State Int = Player.STATE_IDLE, 11 | val videoSize: Rect = Rect(), 12 | val playerError: PlaybackException? = null, 13 | val player: Player? = null, 14 | val isPlaying: Boolean = false 15 | ) 16 | -------------------------------------------------------------------------------- /business/extension/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /business/extension/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/business/extension/consumer-rules.pro -------------------------------------------------------------------------------- /business/extension/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /business/extension/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /business/favorite/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /src/test 3 | /src/androidTest -------------------------------------------------------------------------------- /business/favorite/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.com.android.library) 3 | alias(libs.plugins.org.jetbrains.kotlin.android) 4 | alias(libs.plugins.com.google.devtools.ksp) 5 | alias(libs.plugins.com.google.dagger.hilt.android) 6 | alias(libs.plugins.compose.compiler) 7 | } 8 | 9 | android { 10 | namespace = "com.m3u.business.favorite" 11 | kotlinOptions { 12 | jvmTarget = "17" 13 | } 14 | buildFeatures { 15 | compose = true 16 | } 17 | packaging { 18 | resources.excludes += "META-INF/**" 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation(project(":core")) 24 | implementation(project(":data")) 25 | 26 | implementation(libs.androidx.core.ktx) 27 | 28 | implementation(libs.androidx.lifecycle.runtime.ktx) 29 | implementation(libs.androidx.lifecycle.runtime.compose) 30 | 31 | implementation(libs.google.dagger.hilt) 32 | implementation(libs.androidx.hilt.navigation.compose) 33 | ksp(libs.google.dagger.hilt.compiler) 34 | } 35 | -------------------------------------------------------------------------------- /business/favorite/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/business/favorite/consumer-rules.pro -------------------------------------------------------------------------------- /business/favorite/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /business/favorite/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /business/favorite/src/main/res/drawable/round_play_arrow_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /business/foryou/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /src/test 3 | /src/androidTest -------------------------------------------------------------------------------- /business/foryou/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.com.android.library) 3 | alias(libs.plugins.org.jetbrains.kotlin.android) 4 | alias(libs.plugins.com.google.devtools.ksp) 5 | alias(libs.plugins.com.google.dagger.hilt.android) 6 | alias(libs.plugins.compose.compiler) 7 | } 8 | 9 | android { 10 | namespace = "com.m3u.business.foryou" 11 | kotlinOptions { 12 | jvmTarget = "17" 13 | } 14 | buildFeatures { 15 | compose = true 16 | } 17 | packaging { 18 | resources.excludes += "META-INF/**" 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation(project(":core")) 24 | implementation(project(":data")) 25 | 26 | implementation(libs.androidx.core.ktx) 27 | 28 | implementation(libs.androidx.lifecycle.runtime.ktx) 29 | implementation(libs.androidx.lifecycle.runtime.compose) 30 | 31 | implementation(libs.google.dagger.hilt) 32 | implementation(libs.androidx.hilt.navigation.compose) 33 | ksp(libs.google.dagger.hilt.compiler) 34 | 35 | implementation(libs.androidx.work.runtime.ktx) 36 | } 37 | -------------------------------------------------------------------------------- /business/foryou/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/business/foryou/consumer-rules.pro -------------------------------------------------------------------------------- /business/foryou/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /business/foryou/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /business/foryou/src/main/java/com/m3u/business/foryou/Recommend.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.business.foryou 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.m3u.core.unit.DataUnit 5 | import com.m3u.data.database.model.Channel 6 | import com.m3u.data.database.model.Playlist 7 | 8 | @Immutable 9 | class Recommend( 10 | private val specs: List = emptyList() 11 | ) : AbstractList() { 12 | override val size: Int get() = specs.size 13 | 14 | override fun get(index: Int): Spec = specs[index] 15 | 16 | @Immutable 17 | sealed interface Spec 18 | 19 | @Immutable 20 | data class DiscoverSpec( 21 | val playlist: Playlist, 22 | val category: String 23 | ) : Spec 24 | 25 | @Immutable 26 | data class UnseenSpec( 27 | val channel: Channel 28 | ) : Spec 29 | 30 | @Immutable 31 | data class CwSpec( 32 | val channel: Channel, 33 | val position: Long 34 | ) : Spec 35 | 36 | @Immutable 37 | data class NewRelease( 38 | val name: String, 39 | val description: String, 40 | val downloadCount: Int, 41 | val size: ClosedRange, 42 | val url: String, 43 | ): Spec 44 | } -------------------------------------------------------------------------------- /business/playlist-configuration/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /business/playlist-configuration/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.com.android.library) 3 | alias(libs.plugins.org.jetbrains.kotlin.android) 4 | alias(libs.plugins.com.google.devtools.ksp) 5 | alias(libs.plugins.com.google.dagger.hilt.android) 6 | alias(libs.plugins.compose.compiler) 7 | } 8 | 9 | android { 10 | namespace = "com.m3u.business.playlist.configuration" 11 | kotlinOptions { 12 | jvmTarget = "17" 13 | } 14 | buildFeatures { 15 | compose = true 16 | } 17 | packaging { 18 | resources.excludes += "META-INF/**" 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation(project(":core")) 24 | implementation(project(":data")) 25 | 26 | implementation(libs.androidx.core.ktx) 27 | 28 | implementation(libs.androidx.lifecycle.runtime.ktx) 29 | implementation(libs.androidx.lifecycle.runtime.compose) 30 | 31 | implementation(libs.google.dagger.hilt) 32 | implementation(libs.androidx.hilt.navigation.compose) 33 | ksp(libs.google.dagger.hilt.compiler) 34 | 35 | implementation(libs.androidx.work.runtime.ktx) 36 | } -------------------------------------------------------------------------------- /business/playlist-configuration/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/business/playlist-configuration/consumer-rules.pro -------------------------------------------------------------------------------- /business/playlist-configuration/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /business/playlist-configuration/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /business/playlist/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /src/test 3 | /src/androidTest -------------------------------------------------------------------------------- /business/playlist/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/business/playlist/consumer-rules.pro -------------------------------------------------------------------------------- /business/playlist/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /business/playlist/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /business/playlist/src/main/java/com/m3u/business/playlist/PlaylistMessage.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.business.playlist 2 | 3 | import com.m3u.core.wrapper.Message 4 | import com.m3u.i18n.R.string 5 | import kotlin.time.Duration 6 | import kotlin.time.Duration.Companion.seconds 7 | 8 | sealed class PlaylistMessage( 9 | override val level: Int, 10 | override val type: Int, 11 | override val duration: Duration = 3.seconds, 12 | resId: Int, 13 | vararg formatArgs: Any 14 | ) : Message.Static(level, "playlist", type, duration, resId, formatArgs) { 15 | 16 | data object ChannelNotFound : PlaylistMessage( 17 | level = LEVEL_ERROR, 18 | type = TYPE_SNACK, 19 | resId = string.feat_playlist_error_channel_not_found 20 | ) 21 | 22 | data object ChannelCoverNotFound : PlaylistMessage( 23 | level = LEVEL_ERROR, 24 | type = TYPE_SNACK, 25 | resId = string.feat_playlist_error_channel_cover_not_found 26 | ) 27 | 28 | data class ChannelCoverSaved(val path: String) : PlaylistMessage( 29 | level = LEVEL_INFO, 30 | type = TYPE_SNACK, 31 | resId = string.feat_playlist_success_save_cover, 32 | formatArgs = arrayOf(path) 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /business/playlist/src/main/java/com/m3u/business/playlist/PlaylistNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.business.playlist 2 | 3 | import android.net.Uri 4 | import androidx.navigation.NavController 5 | import androidx.navigation.NavOptions 6 | 7 | const val PLAYLIST_ROUTE_PATH = "playlist_route" 8 | 9 | object PlaylistNavigation { 10 | const val TYPE_URL = "url" 11 | 12 | const val PLAYLIST_ROUTE = 13 | "$PLAYLIST_ROUTE_PATH/{$TYPE_URL}" 14 | 15 | internal fun createPlaylistRoute(url: String): String { 16 | return "$PLAYLIST_ROUTE_PATH/$url" 17 | } 18 | } 19 | 20 | fun NavController.navigateToPlaylist( 21 | playlistUrl: String, 22 | navOptions: NavOptions? = null, 23 | ) { 24 | val encodedUrl = Uri.encode(playlistUrl) 25 | val route = PlaylistNavigation.createPlaylistRoute(encodedUrl) 26 | this.navigate(route, navOptions) 27 | } 28 | -------------------------------------------------------------------------------- /business/playlist/src/main/res/drawable/round_play_arrow_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /business/setting/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /src/test 3 | /src/androidTest -------------------------------------------------------------------------------- /business/setting/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.com.android.library) 3 | alias(libs.plugins.org.jetbrains.kotlin.android) 4 | alias(libs.plugins.com.google.devtools.ksp) 5 | alias(libs.plugins.com.google.dagger.hilt.android) 6 | alias(libs.plugins.compose.compiler) 7 | } 8 | 9 | android { 10 | namespace = "com.m3u.business.setting" 11 | kotlinOptions { 12 | jvmTarget = "17" 13 | } 14 | buildFeatures { 15 | compose = true 16 | } 17 | packaging { 18 | resources.excludes += "META-INF/**" 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation(project(":core")) 24 | implementation(project(":data")) 25 | 26 | implementation(libs.androidx.core.ktx) 27 | 28 | implementation(libs.androidx.lifecycle.runtime.ktx) 29 | implementation(libs.androidx.lifecycle.runtime.compose) 30 | 31 | implementation(libs.google.dagger.hilt) 32 | ksp(libs.google.dagger.hilt.compiler) 33 | implementation(libs.androidx.hilt.navigation.compose) 34 | 35 | implementation(libs.androidx.work.runtime.ktx) 36 | ksp(libs.androidx.hilt.compiler) 37 | implementation(libs.androidx.hilt.work) 38 | } 39 | -------------------------------------------------------------------------------- /business/setting/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/business/setting/consumer-rules.pro -------------------------------------------------------------------------------- /business/setting/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /business/setting/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /business/setting/src/main/java/com/m3u/business/setting/BackingUpAndRestoringState.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.business.setting 2 | 3 | enum class BackingUpAndRestoringState { 4 | NONE, BACKING_UP, RESTORING, BOTH; 5 | 6 | companion object { 7 | fun of(backingUp: Boolean, restoring: Boolean): BackingUpAndRestoringState { 8 | return when { 9 | backingUp && restoring -> BOTH 10 | backingUp -> BACKING_UP 11 | restoring -> RESTORING 12 | else -> NONE 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /business/setting/src/main/res/drawable/telegram.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /compose_compiler_config.conf: -------------------------------------------------------------------------------- 1 | kotlin.collections.* 2 | android.net.Uri 3 | androidx.media3.common.Format 4 | androidx.media3.exoplayer.offline.Download 5 | net.mm2d.upnp.Device 6 | kotlinx.datetime.LocalDateTime -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /src/test 3 | /src/androidTest -------------------------------------------------------------------------------- /core/README.md: -------------------------------------------------------------------------------- 1 | # :core module 2 | 3 | > This module provides core components for other modules 4 | 5 | - annotation: annotation classes. 6 | - architecture: recommend architecture components, be used in modules under feature folder. 7 | - util: extension functions. 8 | - wrapper: universal packaging for other classes. -------------------------------------------------------------------------------- /core/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -dontwarn com.m3u.core.architecture.Abi$$serializer 2 | -dontwarn com.m3u.core.architecture.Abi -------------------------------------------------------------------------------- /core/extension/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/extension/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.com.android.library) 3 | alias(libs.plugins.org.jetbrains.kotlin.android) 4 | alias(libs.plugins.org.jetbrains.kotlin.serialization) 5 | alias(libs.plugins.com.google.devtools.ksp) 6 | alias(libs.plugins.com.google.dagger.hilt.android) 7 | } 8 | 9 | android { 10 | namespace = "com.m3u.core.extension" 11 | kotlinOptions { 12 | jvmTarget = "17" 13 | } 14 | buildFeatures { 15 | aidl = true 16 | } 17 | } 18 | 19 | dependencies { 20 | implementation(libs.m3u.extension.api) 21 | implementation(libs.m3u.extension.annotation) 22 | ksp(libs.m3u.extension.processor) 23 | implementation(project(":data")) 24 | 25 | implementation(libs.androidx.core.ktx) 26 | implementation(libs.androidx.appcompat) 27 | 28 | // hilt 29 | implementation(libs.google.dagger.hilt) 30 | ksp(libs.google.dagger.hilt.compiler) 31 | ksp(libs.androidx.hilt.compiler) 32 | 33 | // auto 34 | implementation(libs.auto.service.annotations) 35 | ksp(libs.auto.service.ksp) 36 | 37 | // wire 38 | implementation("com.squareup.wire:wire-runtime:4.9.2") 39 | } -------------------------------------------------------------------------------- /core/extension/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -keep class com.m3u.core.extension.** { *; } 2 | -keep class com.squareup.wire.** { *; } 3 | -keep class com.m3u.extension.api.model.** { *; } 4 | -keep class * extends com.squareup.wire.ProtoAdapter -------------------------------------------------------------------------------- /core/extension/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -keep class * extends com.m3u.extension.runtime.RemoteModule 24 | 25 | -keepclassmembers class ** { 26 | @com.m3u.extension.runtime.RemoteMethod public *; 27 | @com.m3u.extension.runtime.RemoteMethodParam public *; 28 | } 29 | 30 | -keep class com.m3u.extension.runtime.** { *; } 31 | -------------------------------------------------------------------------------- /core/extension/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/extension/src/main/java/com/m3u/core/extension/OnRemoteCall.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.extension 2 | 3 | import com.m3u.extension.api.IRemoteCallback 4 | 5 | interface OnRemoteCall { 6 | suspend operator fun invoke(module: String, method: String, bytes: ByteArray, callback: IRemoteCallback?) 7 | fun setDependencies(dependencies: RemoteServiceDependencies) 8 | companion object { 9 | const val ERROR_CODE_MODULE_NOT_FOUNDED = -1 10 | const val ERROR_CODE_METHOD_NOT_FOUNDED = -2 11 | const val ERROR_CODE_UNCAUGHT = -3 12 | } 13 | } 14 | 15 | class RemoteCallException( 16 | val errorCode: Int, 17 | val errorMessage: String? 18 | ) : RuntimeException(errorMessage) 19 | -------------------------------------------------------------------------------- /core/extension/src/main/java/com/m3u/core/extension/RemoteServiceDependencies.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.extension 2 | 3 | import com.m3u.data.database.dao.ChannelDao 4 | import com.m3u.data.database.dao.PlaylistDao 5 | 6 | interface RemoteServiceDependencies { 7 | val playlistDao: PlaylistDao 8 | val channelDao: ChannelDao 9 | } -------------------------------------------------------------------------------- /core/extension/src/main/java/com/m3u/core/extension/RemoteServiceDependenciesImpl.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.extension 2 | 3 | import com.google.auto.service.AutoService 4 | import com.m3u.data.database.dao.ChannelDao 5 | import com.m3u.data.database.dao.PlaylistDao 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.EntryPointAccessors 8 | import dagger.hilt.components.SingletonComponent 9 | 10 | @AutoService(RemoteServiceDependencies::class) 11 | class RemoteServiceDependenciesImpl : RemoteServiceDependencies { 12 | @dagger.hilt.EntryPoint 13 | @InstallIn(SingletonComponent::class) 14 | interface EntryPoint { 15 | val playlistDao: PlaylistDao 16 | val channelDao: ChannelDao 17 | } 18 | 19 | private val entryPoint: EntryPoint by lazy { 20 | EntryPointAccessors.fromApplication( 21 | Utils.getContext(), 22 | EntryPoint::class.java 23 | ) 24 | } 25 | 26 | override val playlistDao: PlaylistDao = entryPoint.playlistDao 27 | override val channelDao: ChannelDao = entryPoint.channelDao 28 | } -------------------------------------------------------------------------------- /core/extension/src/main/java/com/m3u/core/extension/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.extension 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import com.m3u.extension.api.model.Result as ProtoResult 6 | 7 | object Utils { 8 | internal fun Result<*>.asProtoResult(): ProtoResult { 9 | return if (isSuccess) { 10 | ProtoResult( 11 | success = true 12 | ) 13 | } else { 14 | ProtoResult( 15 | success = false, 16 | message = this.exceptionOrNull()?.message 17 | ) 18 | } 19 | } 20 | 21 | private lateinit var context: Application 22 | fun init(applicationContext: Application) { 23 | this.context = applicationContext 24 | } 25 | 26 | fun getContext(): Context { 27 | return context 28 | } 29 | } -------------------------------------------------------------------------------- /core/extension/src/main/java/com/m3u/core/extension/business/RemoteModule.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.extension.business 2 | 3 | import com.m3u.core.extension.Utils.asProtoResult 4 | import com.m3u.extension.api.model.Result 5 | import kotlinx.coroutines.CoroutineDispatcher 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | 9 | abstract class RemoteModule( 10 | val dispatcher: CoroutineDispatcher = Dispatchers.Default 11 | ) 12 | 13 | internal suspend inline fun RemoteModule.result( 14 | crossinline block: suspend () -> Unit 15 | ): Result = withContext(dispatcher) { 16 | runCatching { block() }.asProtoResult() 17 | } -------------------------------------------------------------------------------- /core/foundation/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/foundation/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.com.android.library) 3 | alias(libs.plugins.org.jetbrains.kotlin.android) 4 | alias(libs.plugins.compose.compiler) 5 | alias(libs.plugins.com.google.devtools.ksp) 6 | id("kotlin-parcelize") 7 | } 8 | 9 | android { 10 | namespace = "com.m3u.core.foundation" 11 | kotlinOptions { 12 | jvmTarget = "17" 13 | } 14 | } 15 | 16 | dependencies { 17 | // compose 18 | implementation(platform(libs.androidx.compose.bom)) 19 | implementation(libs.androidx.compose.foundation) 20 | implementation(libs.androidx.compose.foundation.layout) 21 | implementation(libs.androidx.compose.material.icons.extended) 22 | implementation(libs.androidx.compose.runtime) 23 | implementation(libs.androidx.compose.ui.util) 24 | implementation(libs.androidx.navigation.compose) 25 | } -------------------------------------------------------------------------------- /core/foundation/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/core/foundation/consumer-rules.pro -------------------------------------------------------------------------------- /core/foundation/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /core/foundation/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/foundation/src/main/java/com/m3u/core/foundation/ktx/NotNulls.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.foundation.ktx 2 | 3 | import kotlin.contracts.ExperimentalContracts 4 | import kotlin.contracts.InvocationKind 5 | import kotlin.contracts.contract 6 | 7 | @OptIn(ExperimentalContracts::class) 8 | inline fun onlyNonNull(t: T?, block: (T) -> Unit) { 9 | contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } 10 | if (t != null) { 11 | block(t) 12 | } 13 | } 14 | @OptIn(ExperimentalContracts::class) 15 | inline fun onlyNonNull(t1: T1?, t2: T2?, block: (T1, T2) -> Unit) { 16 | contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } 17 | if (t1 != null && t2 != null) { 18 | block(t1, t2) 19 | } 20 | } -------------------------------------------------------------------------------- /core/foundation/src/main/java/com/m3u/core/foundation/ui/Composable.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.foundation.ui 2 | 3 | import androidx.compose.runtime.Composable 4 | import kotlin.contracts.ExperimentalContracts 5 | import kotlin.contracts.contract 6 | 7 | @Composable 8 | fun composableOf(block: @Composable () -> Unit): @Composable () -> Unit = block 9 | 10 | @OptIn(ExperimentalContracts::class) 11 | @Composable 12 | fun composableOf(condition: Boolean, block: @Composable () -> Unit): (@Composable () -> Unit)? { 13 | contract { returnsNotNull() implies condition } 14 | return if (condition) { 15 | block 16 | } else { 17 | null 18 | } 19 | } 20 | 21 | @Composable 22 | fun composableOf(block: @Composable S.() -> Unit): @Composable S.() -> Unit = block 23 | 24 | @OptIn(ExperimentalContracts::class) 25 | @Composable 26 | fun composableOf( 27 | condition: Boolean, 28 | block: @Composable S.() -> Unit 29 | ): (@Composable S.() -> Unit)? { 30 | contract { returnsNotNull() implies condition } 31 | return if (condition) { 32 | block 33 | } else { 34 | null 35 | } 36 | } -------------------------------------------------------------------------------- /core/foundation/src/main/java/com/m3u/core/foundation/ui/SugarColors.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.foundation.ui 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | enum class SugarColors(val color: Color, val contentColor: Color) { 6 | Pink(Color(0xfff5cec7), Color.Black), 7 | Red(Color(0xffe79696), Color.Black), 8 | Yellow(Color(0xffffc988), Color.Black), 9 | Orange(Color(0xffffb284), Color.Black), 10 | WaterOrange(Color(0xffefbb95), Color.Black), 11 | SugarOrange(Color(0xffe69e71), Color.Black), 12 | Tee(Color(0xffc4c19c), Color.Black), 13 | Green(Color(0xff87bdae), Color.Black); 14 | operator fun component1(): Color = color 15 | operator fun component2(): Color = contentColor 16 | } -------------------------------------------------------------------------------- /core/foundation/src/main/java/com/m3u/core/foundation/ui/ThenIf.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.foundation.ui 2 | 3 | import androidx.compose.ui.Modifier 4 | 5 | inline fun Modifier.thenIf(condition: Boolean, factory: () -> Modifier): Modifier = then( 6 | if (condition) factory() else Modifier 7 | ) 8 | 9 | inline fun Modifier.notNull(key: T?, factory: (T) -> Modifier): Modifier = then( 10 | if (key != null) factory(key) else Modifier 11 | ) 12 | -------------------------------------------------------------------------------- /core/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/src/main/java/com/m3u/core/Contracts.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core 2 | 3 | object Contracts { 4 | const val PLAYER_ACTIVITY = "com.m3u.smartphone.ui.business.channel.PlayerActivity" 5 | const val PLAYER_SHORTCUT_CHANNEL_ID = "shortcut:channel-id" 6 | const val PLAYER_SHORTCUT_CHANNEL_RECENTLY = "shortcut:channel-recently" 7 | } -------------------------------------------------------------------------------- /core/src/main/java/com/m3u/core/architecture/FileProvider.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.architecture 2 | 3 | import java.io.File 4 | 5 | interface FileProvider { 6 | fun readAll(): List 7 | fun read(path: String): File? 8 | fun write(value: Throwable) 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/java/com/m3u/core/architecture/Signal.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.architecture 2 | 3 | import kotlinx.coroutines.suspendCancellableCoroutine 4 | import kotlin.coroutines.Continuation 5 | import kotlin.coroutines.resume 6 | 7 | object Signal { 8 | private val keys = mutableMapOf>() 9 | suspend fun lock(key: Any): Unit = suspendCancellableCoroutine { continuation -> 10 | if (key in keys) { 11 | throw UnsupportedOperationException("Key already locked: $key") 12 | } 13 | keys[key] = continuation 14 | continuation.invokeOnCancellation { 15 | keys.remove(key) 16 | } 17 | } 18 | 19 | fun unlock(key: Any) { 20 | keys.remove(key)?.resume(Unit) 21 | } 22 | } -------------------------------------------------------------------------------- /core/src/main/java/com/m3u/core/architecture/logger/Profile.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.architecture.logger 2 | 3 | import com.m3u.core.wrapper.Message 4 | 5 | class Profile( 6 | val name: String, 7 | val level: Int = Message.LEVEL_INFO 8 | ) 9 | -------------------------------------------------------------------------------- /core/src/main/java/com/m3u/core/architecture/preferences/ClipMode.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.architecture.preferences 2 | 3 | @Target( 4 | AnnotationTarget.CLASS, 5 | AnnotationTarget.PROPERTY, 6 | AnnotationTarget.VALUE_PARAMETER, 7 | AnnotationTarget.TYPE 8 | ) 9 | @Retention(AnnotationRetention.SOURCE) 10 | annotation class ClipMode { 11 | companion object { 12 | const val ADAPTIVE = 0 13 | const val CLIP = 1 14 | const val STRETCHED = 2 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/java/com/m3u/core/architecture/preferences/ConnectTimeout.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.architecture.preferences 2 | 3 | @Target( 4 | AnnotationTarget.CLASS, 5 | AnnotationTarget.PROPERTY, 6 | AnnotationTarget.VALUE_PARAMETER, 7 | AnnotationTarget.TYPE 8 | ) 9 | @Retention(AnnotationRetention.SOURCE) 10 | annotation class ConnectTimeout { 11 | companion object { 12 | const val SHORT = 8000L 13 | const val LONG = 20000L 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/java/com/m3u/core/architecture/preferences/PlaylistStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.architecture.preferences 2 | 3 | @Target( 4 | AnnotationTarget.CLASS, 5 | AnnotationTarget.PROPERTY, 6 | AnnotationTarget.VALUE_PARAMETER, 7 | AnnotationTarget.TYPE 8 | ) 9 | @Retention(AnnotationRetention.SOURCE) 10 | annotation class PlaylistStrategy { 11 | companion object { 12 | const val ALL = 0 13 | const val KEEP = 1 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/java/com/m3u/core/architecture/preferences/ReconnectMode.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.architecture.preferences 2 | 3 | @Target( 4 | AnnotationTarget.CLASS, 5 | AnnotationTarget.PROPERTY, 6 | AnnotationTarget.VALUE_PARAMETER, 7 | AnnotationTarget.TYPE 8 | ) 9 | @Retention(AnnotationRetention.SOURCE) 10 | annotation class ReconnectMode { 11 | companion object { 12 | const val NO = 0 13 | const val RETRY = 1 14 | const val RECONNECT = 2 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/java/com/m3u/core/architecture/preferences/UnseensMilliseconds.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.architecture.preferences 2 | 3 | @Target( 4 | AnnotationTarget.CLASS, 5 | AnnotationTarget.PROPERTY, 6 | AnnotationTarget.VALUE_PARAMETER, 7 | AnnotationTarget.TYPE 8 | ) 9 | @Retention(AnnotationRetention.SOURCE) 10 | annotation class UnseensMilliseconds { 11 | companion object { 12 | const val DAYS_3 = 3L * 24 * 60 * 60 * 1000 13 | const val DAYS_7 = 7L * 24 * 60 * 60 * 1000 14 | const val DAYS_30 = 30L * 24 * 60 * 60 * 1000 15 | const val NEVER = Long.MAX_VALUE 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/java/com/m3u/core/util/basic/Graphics.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.util.basic 2 | 3 | import android.graphics.Rect 4 | import android.util.Rational 5 | 6 | val Rect.isNotEmpty: Boolean get() = !isEmpty 7 | 8 | val Rect.rational: Rational 9 | get() = Rational(width(), height()).coerceIn( 10 | Rational(100, 239), 11 | Rational(239, 100) 12 | ) 13 | -------------------------------------------------------------------------------- /core/src/main/java/com/m3u/core/util/basic/LetIf.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.util.basic 2 | 3 | import kotlin.contracts.ExperimentalContracts 4 | import kotlin.contracts.InvocationKind 5 | import kotlin.contracts.contract 6 | 7 | @OptIn(ExperimentalContracts::class) 8 | inline fun T.letIf(condition: Boolean, block: (T) -> T): T { 9 | contract { 10 | callsInPlace(block, InvocationKind.AT_MOST_ONCE) 11 | } 12 | return if (condition) block(this) else this 13 | } -------------------------------------------------------------------------------- /core/src/main/java/com/m3u/core/util/basic/Strings.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | package com.m3u.core.util.basic 3 | 4 | import androidx.compose.ui.text.capitalize 5 | import androidx.compose.ui.text.intl.Locale 6 | 7 | fun String.title(): String { 8 | if (this.isEmpty()) return this 9 | val split = this.split(" ") 10 | return split.joinToString( 11 | separator = " ", 12 | transform = { it.capitalize(Locale.current) } 13 | ) 14 | } 15 | 16 | fun String.startsWithAny(vararg prefix: String, ignoreCase: Boolean = false): Boolean { 17 | return prefix.any { startsWith(it, ignoreCase) } 18 | } 19 | 20 | fun String.startWithHttpScheme(): Boolean = startsWithAny( 21 | "http://", "https://", 22 | ignoreCase = true 23 | ) -------------------------------------------------------------------------------- /core/src/main/java/com/m3u/core/util/collections/ForEachNotNull.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.util.collections 2 | 3 | inline fun Array.forEachNotNull(block: (E) -> Unit) { 4 | forEach { 5 | it?.let(block) 6 | } 7 | } 8 | 9 | inline fun Collection.forEachNotNull(block: (E) -> Unit) { 10 | forEach { 11 | it?.let(block) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/java/com/m3u/core/util/collections/IndexOf.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.util.collections 2 | 3 | inline fun Iterator.indexOf(start: Int = 0, predicate: (E) -> Boolean): Int { 4 | var index = 0 5 | while (hasNext()) { 6 | if (index < start) continue 7 | if (predicate(next())) return index 8 | index++ 9 | } 10 | return -1 11 | } 12 | 13 | inline fun List.indexOf(start: Int = 0, predicate: (E) -> Boolean): Int { 14 | var index = start 15 | while (index < lastIndex) { 16 | if (predicate(get(index))) return index 17 | index++ 18 | } 19 | return -1 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/m3u/core/util/compose/ObservableState.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.util.compose 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.snapshots.StateFactoryMarker 6 | 7 | @StateFactoryMarker 8 | fun observableStateOf( 9 | value: T, 10 | onChanged: (T) -> Unit 11 | ): MutableState { 12 | val delegate = mutableStateOf(value) 13 | return ObservableState(delegate, onChanged) 14 | } 15 | 16 | private class ObservableState( 17 | private val delegate: MutableState, 18 | private val onChanged: (T) -> Unit 19 | ) : MutableState { 20 | override var value: T 21 | get() = delegate.value 22 | set(value) { 23 | onChanged(value) 24 | delegate.value = value 25 | } 26 | 27 | override fun component1(): T = delegate.component1() 28 | override fun component2(): (T) -> Unit = delegate.component2() 29 | } 30 | -------------------------------------------------------------------------------- /core/src/main/java/com/m3u/core/util/context/Configuration.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.util.context 2 | 3 | import android.content.res.Configuration 4 | 5 | val Configuration.isPortraitMode: Boolean 6 | get() = orientation == Configuration.ORIENTATION_PORTRAIT 7 | 8 | val Configuration.isDarkMode: Boolean 9 | get() = uiMode == Configuration.UI_MODE_NIGHT_YES 10 | -------------------------------------------------------------------------------- /core/src/main/java/com/m3u/core/util/context/Toasts.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.util.context 2 | 3 | import android.content.Context 4 | import android.os.Handler 5 | import android.os.Looper 6 | import android.widget.Toast 7 | 8 | fun Context.toast(message: String, duration: Int = Toast.LENGTH_SHORT) { 9 | Handler(Looper.getMainLooper()).post { 10 | Toast.makeText(this, message, duration).show() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /core/src/main/java/com/m3u/core/wrapper/Sort.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.core.wrapper 2 | 3 | import com.m3u.i18n.R.string 4 | import androidx.annotation.StringRes 5 | import androidx.compose.runtime.Immutable 6 | 7 | @Immutable 8 | enum class Sort(@StringRes val resId: Int) { 9 | UNSPECIFIED(string.ui_sort_unspecified), 10 | ASC(string.ui_sort_asc), 11 | DESC(string.ui_sort_desc), 12 | RECENTLY(string.ui_sort_recently), 13 | MIXED(string.ui_sort_mixed) 14 | } -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /src/test 3 | /src/androidTest -------------------------------------------------------------------------------- /data/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/data/consumer-rules.pro -------------------------------------------------------------------------------- /data/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -dontwarn java.lang.invoke.StringConcatFactory 24 | -dontwarn com.m3u.i18n.R$string -------------------------------------------------------------------------------- /data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/Certs.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data 2 | 3 | import android.annotation.SuppressLint 4 | import java.security.cert.X509Certificate 5 | import javax.net.ssl.X509TrustManager 6 | 7 | internal object Certs { 8 | val TrustAll by lazy { 9 | @SuppressLint("CustomX509TrustManager") 10 | object : X509TrustManager { 11 | @SuppressLint("TrustAllX509TrustManager") 12 | override fun checkClientTrusted( 13 | chain: Array?, 14 | authType: String? 15 | ) { 16 | // do nothing 17 | } 18 | 19 | @SuppressLint("TrustAllX509TrustManager") 20 | override fun checkServerTrusted( 21 | chain: Array?, 22 | authType: String? 23 | ) { 24 | // do nothing 25 | } 26 | 27 | override fun getAcceptedIssuers(): Array { 28 | return emptyArray() 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/SSLs.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data 2 | 3 | import java.security.SecureRandom 4 | import javax.net.ssl.SSLContext 5 | 6 | internal object SSLs { 7 | val TLSTrustAll: SSLContext by lazy { 8 | SSLContext.getInstance("TLS").apply { 9 | init(null, arrayOf(Certs.TrustAll), SecureRandom()) 10 | } 11 | } 12 | val SSLTrustAll: SSLContext by lazy { 13 | SSLContext.getInstance("SSL").apply { 14 | init(null, arrayOf(Certs.TrustAll), SecureRandom()) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/api/BaseUrls.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.api 2 | 3 | object BaseUrls { 4 | const val GITHUB_BASE_URL = "https://api.github.com" 5 | } 6 | -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/api/dto/github/Asset.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.api.dto.github 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Asset( 8 | @SerialName("browser_download_url") 9 | val browserDownloadUrl: String, 10 | @SerialName("content_type") 11 | val contentType: String, 12 | @SerialName("created_at") 13 | val createdAt: String, 14 | @SerialName("download_count") 15 | val downloadCount: Int, 16 | @SerialName("id") 17 | val id: Int, 18 | @SerialName("name") 19 | val name: String, 20 | @SerialName("node_id") 21 | val nodeId: String, 22 | @SerialName("size") 23 | val size: Int, 24 | @SerialName("state") 25 | val state: String, 26 | @SerialName("updated_at") 27 | val updatedAt: String, 28 | @SerialName("uploader") 29 | val uploader: User, 30 | @SerialName("url") 31 | val url: String 32 | ) 33 | -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/api/dto/github/File.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | package com.m3u.data.api.dto.github 3 | 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class File( 9 | @SerialName("_links") 10 | val links: Links, 11 | @SerialName("download_url") 12 | val downloadUrl: String = "", 13 | @SerialName("git_url") 14 | val gitUrl: String = "", 15 | @SerialName("html_url") 16 | val htmlUrl: String = "", 17 | @SerialName("name") 18 | val name: String, 19 | @SerialName("path") 20 | val path: String, 21 | @SerialName("sha") 22 | val sha: String, 23 | @SerialName("size") 24 | val size: Int, 25 | @SerialName("type") 26 | val type: String, 27 | @SerialName("url") 28 | val url: String 29 | ) { 30 | companion object { 31 | const val TYPE_DIR = "dir" 32 | const val TYPE_FILE = "file" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/api/dto/github/Leaf.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.api.dto.github 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Leaf( 7 | val mode: String, 8 | val path: String, 9 | val sha: String, 10 | val size: Int, 11 | val type: String, 12 | val url: String 13 | ) 14 | -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/api/dto/github/Links.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.api.dto.github 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Links( 8 | @SerialName("git") 9 | val git: String, 10 | @SerialName("html") 11 | val html: String, 12 | @SerialName("self") 13 | val self: String 14 | ) 15 | -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/api/dto/github/Tree.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.api.dto.github 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Tree( 8 | val sha: String, 9 | @SerialName("tree") 10 | val leaves: List, 11 | val truncated: Boolean, 12 | val url: String 13 | ) 14 | -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/database/Converters.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.database 2 | 3 | import androidx.room.TypeConverter 4 | import com.m3u.data.database.model.DataSource 5 | import kotlinx.serialization.encodeToString 6 | import kotlinx.serialization.json.Json 7 | 8 | object Converters { 9 | @TypeConverter 10 | fun fromStringList(from: List): String { 11 | return Json.encodeToString(from) 12 | } 13 | 14 | @TypeConverter 15 | fun toStringList(to: String): List { 16 | return Json.decodeFromString(to) 17 | } 18 | 19 | @TypeConverter 20 | fun fromDataSource(from: DataSource): String = from.value 21 | 22 | @TypeConverter 23 | fun toDataSource(to: String): DataSource = DataSource.of(to) 24 | } -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/database/dao/ColorSchemeDao.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.database.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.OnConflictStrategy 7 | import androidx.room.Query 8 | import com.m3u.data.database.model.ColorScheme 9 | import kotlinx.coroutines.flow.Flow 10 | 11 | @Dao 12 | interface ColorSchemeDao { 13 | @Query("SELECT * FROM color_pack") 14 | fun observeAll(): Flow> 15 | 16 | @Insert(onConflict = OnConflictStrategy.REPLACE) 17 | suspend fun insert(colorScheme: ColorScheme) 18 | 19 | @Insert(onConflict = OnConflictStrategy.REPLACE) 20 | suspend fun insertAll(vararg colorScheme: ColorScheme) 21 | 22 | @Delete 23 | suspend fun delete(colorScheme: ColorScheme) 24 | } -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/database/dao/EpisodeDao.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.database.dao 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.room.Dao 5 | import androidx.room.Query 6 | import com.m3u.data.database.model.Episode 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | @Dao 10 | interface EpisodeDao { 11 | @Query("SELECT * FROM episodes WHERE series_id = :seriesId ORDER BY number") 12 | fun observeAllBySeriesId(seriesId: Int): Flow 13 | 14 | @Query("SELECT * FROM episodes WHERE series_id = :seriesId ORDER BY number") 15 | fun pagingAllBySeriesId(seriesId: Int): PagingSource 16 | } -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/database/example/ColorSchemeExample.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.database.example 2 | 3 | import androidx.sqlite.db.SupportSQLiteDatabase 4 | import com.m3u.data.database.model.ColorScheme 5 | 6 | object ColorSchemeExample { 7 | val schemes = listOf( 8 | ColorScheme(0x5E6738, false, "avocado"), 9 | ColorScheme(0x5E6738, true, "mint"), 10 | ColorScheme(0xe69e71, false, "orange"), 11 | ColorScheme(0xe69e71, true, "leather"), 12 | ColorScheme(0xce5b73, false, "cherry"), 13 | ColorScheme(0xce5b73, true, "raspberry"), 14 | ) 15 | 16 | operator fun invoke( 17 | db: SupportSQLiteDatabase, 18 | schemes: List = this.schemes 19 | ) { 20 | val values = schemes.joinToString( 21 | postfix = ";" 22 | ) { 23 | """('${it.argb}', '${if (it.isDark) 1 else 0}', '${it.name}')""" 24 | } 25 | 26 | db.execSQL( 27 | """ 28 | INSERT INTO 'color_pack' ('argb', 'dark', 'name') 29 | VALUES 30 | $values 31 | """.trimIndent() 32 | ) 33 | } 34 | } -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/database/model/AdjacentChannels.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.database.model 2 | 3 | import androidx.room.ColumnInfo 4 | 5 | data class AdjacentChannels( 6 | @ColumnInfo("prev_id") 7 | val prevId: Int?, 8 | @ColumnInfo("next_id") 9 | val nextId: Int? 10 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/database/model/ColorScheme.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.database.model 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.room.ColumnInfo 5 | import androidx.room.Entity 6 | 7 | @Entity( 8 | tableName = "color_pack", 9 | primaryKeys = ["argb", "dark"] 10 | ) 11 | @Immutable 12 | data class ColorScheme( 13 | @ColumnInfo("argb") 14 | val argb: Int, 15 | @ColumnInfo("dark") 16 | val isDark: Boolean, 17 | @ColumnInfo("name") 18 | val name: String, 19 | ) { 20 | companion object { 21 | const val NAME_TEMP = "temp" 22 | } 23 | } -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/database/model/Episode.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.database.model 2 | 3 | import androidx.annotation.Keep 4 | import androidx.compose.runtime.Immutable 5 | import androidx.room.ColumnInfo 6 | import androidx.room.Entity 7 | import androidx.room.PrimaryKey 8 | 9 | @Entity(tableName = "episodes") 10 | @Immutable 11 | @Keep 12 | // for series type channels 13 | data class Episode( 14 | @ColumnInfo(name = "title") 15 | val title: String, 16 | // series is a special channel 17 | // if a playlist type is one of [Playlist.SERIES_TYPES] 18 | // then its all channels are series. 19 | @ColumnInfo(name = "series_id") 20 | val seriesId: Int, 21 | @ColumnInfo(name = "season") 22 | val season: String, 23 | @ColumnInfo(name = "number") 24 | val number: Int, 25 | @ColumnInfo(name = "url") 26 | val url: String, 27 | @PrimaryKey 28 | @ColumnInfo(name = "id") 29 | val id: Int = 0 30 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/model/ChannelSet.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.model 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.m3u.core.wrapper.Sort 5 | 6 | @Immutable 7 | data class ChannelSet( 8 | val playlistUrl: String, 9 | val query: String? = null, 10 | val sort: Sort = Sort.UNSPECIFIED, 11 | val category: String? = null 12 | ) 13 | -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/parser/ParserModule.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | package com.m3u.data.parser 3 | 4 | import com.m3u.data.parser.epg.EpgParser 5 | import com.m3u.data.parser.epg.EpgParserImpl 6 | import com.m3u.data.parser.m3u.M3UParser 7 | import com.m3u.data.parser.m3u.M3UParserImpl 8 | import com.m3u.data.parser.xtream.XtreamParser 9 | import com.m3u.data.parser.xtream.XtreamParserImpl 10 | import dagger.Binds 11 | import dagger.Module 12 | import dagger.hilt.InstallIn 13 | import dagger.hilt.components.SingletonComponent 14 | 15 | @Module 16 | @InstallIn(SingletonComponent::class) 17 | internal interface ParserModule { 18 | @Binds 19 | fun bindM3UParser(parser: M3UParserImpl): M3UParser 20 | 21 | @Binds 22 | fun bindXtreamParser(parser: XtreamParserImpl): XtreamParser 23 | 24 | @Binds 25 | fun bindEpgParser(parser: EpgParserImpl): EpgParser 26 | } 27 | -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/parser/epg/EpgParser.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.parser.epg 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import java.io.InputStream 5 | 6 | interface EpgParser { 7 | fun readProgrammes( 8 | input: InputStream 9 | ): Flow 10 | } -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/parser/m3u/M3UParser.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.parser.m3u 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import java.io.InputStream 5 | 6 | internal interface M3UParser { 7 | fun parse(input: InputStream): Flow 8 | } 9 | -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/parser/xtream/XtreamCategory.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.parser.xtream 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class XtreamCategory( 8 | @SerialName("category_id") 9 | val categoryId: Int?, 10 | @SerialName("category_name") 11 | val categoryName: String?, 12 | @SerialName("parent_id") 13 | val parentId: Int? 14 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/parser/xtream/XtreamOutput.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.parser.xtream 2 | 3 | data class XtreamOutput( 4 | val liveCategories: List = emptyList(), 5 | val vodCategories: List = emptyList(), 6 | val serialCategories: List = emptyList(), 7 | val allowedOutputFormats: List = emptyList(), 8 | val serverProtocol: String = "http", 9 | val port: Int? = null 10 | ) 11 | -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/repository/BackupOrRestoreContracts.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.repository 2 | 3 | internal object BackupOrRestoreContracts { 4 | fun wrapPlaylist(encoded: String): String = "P,${encoded.trim()}" 5 | fun wrapChannel(encoded: String): String = "S,${encoded.trim()}" 6 | fun unwrapPlaylist(wrapped: String): String? { 7 | val trimmed = wrapped.trim() 8 | if (!trimmed.startsWith("P,")) return null 9 | return trimmed.drop(2) 10 | } 11 | 12 | fun unwrapChannel(wrapped: String): String? { 13 | val trimmed = wrapped.trim() 14 | if (!trimmed.startsWith("S,")) return null 15 | return trimmed.drop(2) 16 | } 17 | } -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/repository/CoroutineCache.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.repository 2 | 3 | import kotlinx.coroutines.sync.Mutex 4 | import kotlinx.coroutines.sync.withLock 5 | 6 | internal fun createCoroutineCache( 7 | limit: Int, 8 | onReceived: suspend (cache: List) -> Unit 9 | ): CoroutineCache = object : CoroutineCache(limit) { 10 | override suspend fun onReceived(cache: List) { 11 | onReceived(cache) 12 | } 13 | } 14 | 15 | internal abstract class CoroutineCache( 16 | private val limit: Int 17 | ) { 18 | private val cache = mutableListOf() 19 | private val mutex = Mutex() 20 | abstract suspend fun onReceived(cache: List) 21 | suspend fun push(element: E) { 22 | cache += element 23 | if (cache.size >= limit) { 24 | mutex.withLock { 25 | // check again 26 | if (cache.size >= limit) { 27 | onReceived(cache) 28 | cache.clear() 29 | } 30 | } 31 | } 32 | } 33 | 34 | suspend fun flush() { 35 | mutex.withLock { 36 | onReceived(cache) 37 | cache.clear() 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/repository/media/MediaRepository.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.repository.media 2 | 3 | import android.graphics.drawable.Drawable 4 | import android.net.Uri 5 | import io.ktor.utils.io.ByteReadChannel 6 | import java.io.File 7 | import java.io.InputStream 8 | import java.io.OutputStream 9 | 10 | interface MediaRepository { 11 | suspend fun savePicture(url: String): File 12 | fun openOutputStream(uri: Uri): OutputStream? 13 | fun openInputStream(uri: Uri): InputStream? 14 | 15 | suspend fun loadDrawable(url: String): Drawable? 16 | suspend fun installApk(channel: ByteReadChannel) 17 | } 18 | -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/repository/programme/ProgrammeRepository.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.repository.programme 2 | 3 | import androidx.paging.PagingData 4 | import com.m3u.data.database.model.Programme 5 | import com.m3u.data.database.model.ProgrammeRange 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.StateFlow 8 | 9 | interface ProgrammeRepository { 10 | fun pagingProgrammes( 11 | playlistUrl: String, 12 | relationId: String 13 | ): Flow> 14 | 15 | fun observeProgrammeRange( 16 | playlistUrl: String, 17 | relationId: String 18 | ): Flow 19 | 20 | fun observeProgrammeRange( 21 | playlistUrl: String 22 | ): Flow 23 | 24 | val refreshingEpgUrls: StateFlow> 25 | fun checkOrRefreshProgrammesOrThrow( 26 | vararg playlistUrls: String, 27 | ignoreCache: Boolean 28 | ): Flow 29 | 30 | suspend fun getById(id: Int): Programme? 31 | suspend fun getProgrammeCurrently(channelId: Int): Programme? 32 | } -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/service/internal/Codecs.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.service.internal 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.net.Uri 6 | import androidx.media3.exoplayer.DefaultRenderersFactory 7 | import androidx.media3.exoplayer.RenderersFactory 8 | import io.github.anilbeesetti.nextlib.media3ext.ffdecoder.NextRenderersFactory 9 | import io.github.anilbeesetti.nextlib.mediainfo.MediaInfoBuilder 10 | 11 | object Codecs { 12 | fun createRenderersFactory(context: Context): RenderersFactory { 13 | return NextRenderersFactory(context).apply { 14 | setEnableDecoderFallback(true) 15 | setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) 16 | } 17 | } 18 | 19 | fun getThumbnail(context: Context, uri: Uri): Bitmap? { 20 | val mediaInfo = MediaInfoBuilder() 21 | .from(context, uri) 22 | .build() 23 | val frame = mediaInfo?.getFrame() 24 | mediaInfo?.release() 25 | return frame 26 | } 27 | } -------------------------------------------------------------------------------- /data/src/main/java/com/m3u/data/service/internal/KodiAdaptions.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.data.service.internal 2 | 3 | internal object KodiAdaptions { 4 | const val HTTP_OPTION_UA = "user-agent" 5 | } -------------------------------------------------------------------------------- /data/src/main/res/drawable/baseline_notifications_none_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /data/src/main/res/drawable/round_cancel_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /data/src/main/res/drawable/round_file_download_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /data/src/main/res/drawable/round_refresh_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 |

M3U is a stream media player on Android devices. Android 8.0 and above supported.

2 |


Features:

3 |
    4 |
  • M3U and M3U8 files.
  • 5 |
  • HTTPS and RTMP stream.
  • 6 |
  • Android TV.
  • 7 |
8 |

More to come.

-------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/featureGraphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/fastlane/metadata/android/en-US/images/featureGraphic.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/tvScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/fastlane/metadata/android/en-US/images/tvScreenshots/1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/tvScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/fastlane/metadata/android/en-US/images/tvScreenshots/2.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/tvScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/fastlane/metadata/android/en-US/images/tvScreenshots/3.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | FREE stream media player for android. -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | M3U -------------------------------------------------------------------------------- /fastlane/metadata/android/es-ES/full_description.txt: -------------------------------------------------------------------------------- 1 |

M3U es un reproductor de streaming LIBRE para Android. Para Android 8.0 y posteriores.

2 |


Características:

3 |
    4 |
  • Archivos M3U y M3U8.
  • 5 |
  • Transmisión HTTPS y RTMP.
  • 6 |
  • Android TV.
  • 7 |
8 |

Más en camino.

-------------------------------------------------------------------------------- /fastlane/metadata/android/es-ES/short_description.txt: -------------------------------------------------------------------------------- 1 | Reproductor de streaming LIBRE para Android. -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Apr 03 22:02:28 CST 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /i18n/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /i18n/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.com.android.library) 3 | alias(libs.plugins.org.jetbrains.kotlin.android) 4 | } 5 | 6 | android { 7 | namespace = "com.m3u.i18n" 8 | kotlinOptions { 9 | jvmTarget = "17" 10 | } 11 | } 12 | 13 | dependencies { 14 | implementation(libs.androidx.core.ktx) 15 | implementation(libs.androidx.appcompat) 16 | } -------------------------------------------------------------------------------- /i18n/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/i18n/consumer-rules.pro -------------------------------------------------------------------------------- /i18n/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /i18n/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-de-rDE/app.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | App-Abstürze 4 | Kürzlich 5 | Spiele kürzlichen Stream 6 | unerreichbar 7 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-de-rDE/data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Datei nicht gefunden 4 | Playlist-Name ist leer 5 | Stream Download Service 6 | Stream Download Beschreibung 7 | 8 | Abbrechen 9 | Wiederholen 10 | Komplett (+%d) 11 | %d Kanäle wurden heruntergeladen 12 | %d Programme wurden heruntergeladen 13 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-de-rDE/feat_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Über das Projekt 4 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-de-rDE/feat_console.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Konsolen Editor 4 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-de-rDE/feat_favourite.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | unbekannt 4 | Zufällige Wiedergabe 5 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-de-rDE/feat_foryou.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | versteckte Streams 4 | löschen 5 | URL kopieren 6 | umbenennen 7 | importiert 8 | Playlist hinzufügen 9 | Favorit, den Du erneut sehen möchtest 10 | mehr als %d Tage 11 | %d Tage 12 | %d Stunden 13 | Code vom Fernseher eingeben 14 | Stelle sicher, dass Du mit dem selben WLAN verbunden bist 15 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-de-rDE/feat_playlist.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | unbekannt 4 | like 5 | like entfernen 6 | verstecken 7 | speichern in der Galerie 8 | Verknüpfung erstellen 9 | Playlist existiert nicht (%s) 10 | Schlüsselwort eingeben 11 | Playlist URL existiert nicht 12 | Cover existiert nicht 13 | Stream existiert nicht 14 | speichere in (%s) 15 | hochscrollen 16 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-de-rDE/feat_playlist_configuration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Titel 4 | User-Agent 5 | aktiviere EPGs 6 | Synchronisiere EPG... 7 | Ablaufdatum: %s 8 | EPG ist veraltet, bitte erneut synchronisieren 9 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-de-rDE/ui.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | M3U 4 | Favoriten 5 | Einstellungen 6 | 7 | Home 8 | Favoriten 9 | Einstellungen 10 | 11 | unbekannter Fehler 12 | zurück 13 | 14 | ho 15 | la 16 | 17 | Sortieren 18 | A-Z 19 | Z-A 20 | Kürzlich 21 | nie gespielt 22 | Nicht spezifiziert 23 | 24 | Verbinden 25 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-es-rES/app.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | fallos de la app 4 | Reciente 5 | Reproduce la transmisión reciente 6 | no está disponible 7 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-es-rES/data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | el archivo no fue encontrado 4 | la lista de reproducción no tiene nombre 5 | Servicio de descarga de transmisión 6 | Descripción de descarga de transmisión 7 | 8 | Cancelar 9 | Reintentar 10 | Completado (+%d) 11 | %d canales fueron descargados 12 | %d programas fueron descargados 13 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-es-rES/feat_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | acerca del proyecto 4 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-es-rES/feat_console.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Console Editor 4 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-es-rES/feat_favourite.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | desconocido 4 | reproducir aleatoriamente 5 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-es-rES/feat_playlist_configuration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | título 4 | agente de usuario 5 | EPGs habilitadas 6 | sinc. programación 7 | cancelar sinc. programación 8 | Expira: %s 9 | la programación está desactualizada 10 | Autorefrescar programación 11 | Cuando se inicializa la app 12 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-es-rMX/app.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | errores de la app 4 | Reciente 5 | Reproduce la emisión reciente 6 | no disponible 7 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-es-rMX/data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | no se encuentra el archivo 4 | playlist sin nombre 5 | Servicio de descarga de emisión 6 | Descripción de descarga de emisión 7 | 8 | Cancelar 9 | Reintentar 10 | Completado (+%d) 11 | %d canales se descargaron 12 | %d programas se descargaron 13 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-es-rMX/feat_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | sobre el proyecto 4 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-es-rMX/feat_console.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Console Editor 4 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-es-rMX/feat_favourite.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | desconocido 4 | reproducir al azar 5 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-es-rMX/feat_playlist.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | desconocido 4 | añadir a favoritos 5 | quitar de favoritos 6 | ocultar 7 | guardar a álbum 8 | crear atajo 9 | la playlist no existe (%s) 10 | ingresa la palabra clave 11 | la url de la playlist no existe 12 | la carátula no existe 13 | la emisión no existe 14 | guardada a (%s) 15 | subir 16 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-es-rMX/feat_playlist_configuration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | título 4 | agente de usuario 5 | EPGs activadas 6 | sinc. programas 7 | cancelar sinc. programación 8 | Expiran: %s 9 | los programas está desactualizados 10 | Autorefrescar programas 11 | Cuando se inicia la app 12 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-id-rID/app.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Aplikasi Tidak Berfungsi 4 | Baru saja 5 | Putar yang Baru Saja Diputar 6 | Tidak Tersedia 7 | 8 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-id-rID/data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | file tidak ditemukan 4 | Nama Playlist kosong 5 | Layanan Unduhan Streaming 6 | Deskripsi Layanan Unduhan Streaming 7 | 8 | Batal 9 | Coba Ulang 10 | Selesai (+%d) 11 | %d saluran telah diunduh 12 | %d program telah diunduh 13 | 14 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-id-rID/feat_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Tentang Projek 4 | 5 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-id-rID/feat_console.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Editor Konsol 4 | 5 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-id-rID/feat_favourite.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Tidak diketahui 4 | Putar acak 5 | 6 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-id-rID/feat_playlist_configuration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Judul 4 | user agent 5 | EPG aktif 6 | sinkronkan program 7 | batalkan sinkronisasi program 8 | Kedaluwarsa: %s 9 | Cache program telah kedaluwarsa 10 | Muat ulang program otomatis 11 | Saat aplikasi dimulai 12 | 13 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-pt-rBR/app.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Falhas do App 4 | Recentes 5 | Veja as streams recentes 6 | Indisponível 7 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-pt-rBR/data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Arquivo não encontrado 4 | O nome da playlist está vazio 5 | Serviço de download de streaming 6 | Descrição do download da stream 7 | 8 | Cancelar 9 | Tentar novamente 10 | %d Canais foram baixados 11 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-pt-rBR/feat_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sobre o projeto 4 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-pt-rBR/feat_console.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Editor de console 4 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-pt-rBR/feat_favourite.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Desconhecido 4 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-pt-rBR/feat_foryou.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Streams ocultas 4 | Cancelar inscrição 5 | Copiar url 6 | Renomear 7 | Importada 8 | Adicione uma playlist 9 | Favoritos que você veria novamente 10 | mais de %d dias 11 | %d dias 12 | %d horas 13 | Digite o código da TV 14 | Certifique-se de está conectado ao mesmo Wi-Fi 15 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-pt-rBR/feat_playlist.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Desconhecido 4 | Favoritar 5 | Desfavoritar 6 | Ocultar 7 | Salvar na galeria 8 | Criar um atalho 9 | A playlist não existe (%s) 10 | Insira a palavra-chave 11 | A URL da playlist não existe 12 | A capa não existe 13 | A stream não existe 14 | Salvo em (%s) 15 | Role para cima 16 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-pt-rBR/feat_playlist_configuration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Nome 4 | Agente de usuário 5 | EPGs habilitados 6 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-pt-rBR/ui.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | M3U 4 | Favoritos 5 | Configurações 6 | 7 | Para Você 8 | Favoritos 9 | Configurações 10 | 11 | Erro Desconhecido 12 | Voltar 13 | 14 | ho 15 | la 16 | 17 | Reordenar 18 | A-Z 19 | Z-A 20 | Recentes 21 | Não assistiu ainda 22 | Não especificado 23 | 24 | Conectar 25 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-ro-rRO/app.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | erori aplicatie 4 | Recent 5 | Ruleaaz canale recente 6 | indisponibil 7 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-ro-rRO/data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | fisier inexistent 4 | nume lista necompletat 5 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-ro-rRO/feat_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | despre proiect 4 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-ro-rRO/feat_console.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Editor Consola 4 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-ro-rRO/feat_favourite.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | necunoscut 4 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-ro-rRO/feat_foryou.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | streamuri ascunse 4 | dezabonare 5 | copie adresa lista 6 | redenumire 7 | importat 8 | adauga o lista 9 | favorite pe care le vei mai vedea 10 | mai mult de %d zile 11 | %d zile 12 | %d ore 13 | codul de pe TV 14 | Verificati sa fiti conectat la aceasi retea Wi-Fi 15 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-ro-rRO/feat_playlist.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | necunoscut 4 | favorit 5 | anulare favorit 6 | ascunde 7 | salvare in galerie 8 | creare scurtatura 9 | lista nu exista (%s) 10 | introdu cuvantul cautat 11 | adresa lista nu exista 12 | coperta nu exista 13 | canalul nu exista 14 | salvat in (%s) 15 | deruleazA sus 16 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-ro-rRO/ui.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | M3U 4 | Favorite 5 | Setari 6 | 7 | Pentru Tine 8 | Favorite 9 | Setari 10 | 11 | eroare necunoscuta 12 | inapoi 13 | 14 | stanga 15 | dreapta 16 | 17 | Aranjare 18 | A-Z 19 | Z-A 20 | recent 21 | niciodata rulat 22 | Nespecificat 23 | 24 | Conectare 25 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-tr-rTR/app.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | uygulama çöktü 4 | Son İzlenen 5 | Son izlenen kanalı oynat 6 | kullanılamıyor 7 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-tr-rTR/data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dosya bulunamadı 4 | Oynatma listesi adı boş 5 | Akış İndirme Servisi 6 | Akış indirme servisi açıklaması 7 | 8 | İptal Et 9 | Yeniden Dene 10 | Tamamlandı (+%d) 11 | %d kanal indirildi 12 | %d program indirildi 13 | 14 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-tr-rTR/feat_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Proje Hakkında 4 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-tr-rTR/feat_console.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Konsol Editörü 4 | 5 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-tr-rTR/feat_favourite.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bilinmiyor 4 | Rastgele oynat 5 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-tr-rTR/feat_playlist_configuration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Başlık 4 | Kullanıcı aracısı 5 | Etkin EPGler 6 | Programları eşitle 7 | Eşitlemeyi iptal et 8 | Süresi doldu: %s 9 | Önbellekteki programların süresi dolmuş 10 | Programları Otomatik Yenile 11 | Uygulama başlatıldığında 12 | 13 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-zh-rCN/app.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 应用发生崩溃 4 | 最近 5 | 播放最近的播放过的频道 6 | 不可用 7 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-zh-rCN/data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 文件不存在 4 | 订阅名称为空 5 | 6 | 取消 7 | 重试 8 | 完成(+%d) 9 | 已下载 %d 个频道 10 | 已下载 %d 个节目 11 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-zh-rCN/feat_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 关于项目 4 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-zh-rCN/feat_console.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Console Editor 4 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-zh-rCN/feat_favourite.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 未知 4 | 随机播放 5 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-zh-rCN/feat_foryou.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 屏蔽的频道 4 | 取消订阅 5 | 复制链接 6 | 重命名 7 | 导入频道 8 | 添加一个播放列表 9 | 好久不见 10 | 超过 %d 天 11 | %d 天 12 | %d 小时 13 | 继续播放 14 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-zh-rCN/feat_playlist.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 未知 4 | 喜欢 5 | 取消喜欢 6 | 屏蔽 7 | 保存封面 8 | 创建快捷方式 9 | 10 | 输入关键字 11 | 12 | 订阅不存在(%s) 13 | 订阅不存在 14 | 封面不存在 15 | 频道不存在 16 | 保存到了(%s) 17 | 回到顶部 18 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-zh-rCN/feat_playlist_configuration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 标题 4 | 用户代理(UA) 5 | 启用的 EPG 6 | 同步节目 7 | 过期时间: %s 8 | 缓存的节目已经过时 9 | -------------------------------------------------------------------------------- /i18n/src/main/res/values-zh-rCN/ui.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | M3U 4 | 5 | 主页 6 | 喜欢 7 | 设置 8 | 9 | 设置 10 | 喜欢 11 | 12 | 未知错误 13 | 返回 14 | 15 | 你知道吗? 16 | 我们会记录您的**观看进度**,并在下次播放时恢复进度 17 | 18 | 排序 19 | A-Z 20 | Z-A 21 | 最近 22 | 混合 23 | 从未播放过 24 | 默认 25 | -------------------------------------------------------------------------------- /i18n/src/main/res/values/app.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | app crashes 4 | Recently 5 | Play recently channel 6 | unavailable 7 | -------------------------------------------------------------------------------- /i18n/src/main/res/values/data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | file not found 4 | playlist name is empty 5 | Stream Download Service 6 | Stream Download Description 7 | 8 | Cancel 9 | Retry 10 | Completed (+%d) 11 | %d channels have been downloaded 12 | %d programmes have been downloaded 13 | -------------------------------------------------------------------------------- /i18n/src/main/res/values/feat_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | about project 4 | -------------------------------------------------------------------------------- /i18n/src/main/res/values/feat_console.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Console Editor 4 | -------------------------------------------------------------------------------- /i18n/src/main/res/values/feat_favourite.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | unknown 4 | play randomly 5 | -------------------------------------------------------------------------------- /i18n/src/main/res/values/feat_foryou.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | hidden channels 4 | unsubscribe 5 | copy url 6 | rename 7 | imported 8 | add a playlist 9 | favourite that you would see again 10 | more than %d days 11 | %d days 12 | %d hours 13 | continue watching 14 | new release 15 | enter code from TV 16 | Make sure to connect to the same Wi-Fi 17 | -------------------------------------------------------------------------------- /i18n/src/main/res/values/feat_playlist.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | unknown 4 | like 5 | cancel like 6 | hide 7 | save to gallery 8 | create shortcut 9 | playlist is not existed (%s) 10 | enter key word 11 | playlist url is not existed 12 | cover is not existed 13 | channel is not existed 14 | saved to (%s) 15 | scroll up 16 | -------------------------------------------------------------------------------- /i18n/src/main/res/values/feat_playlist_configuration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | title 4 | user agent 5 | enabled EPGs 6 | sync programmes 7 | cancel sync programmes 8 | Expire: %s 9 | Cached programmes are out of date 10 | Auto Refresh Programmes 11 | When App Startup 12 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk17 3 | before_install: 4 | - ./scripts/prepareJitpackEnvironment.sh -------------------------------------------------------------------------------- /lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /lint/annotation/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /lint/annotation/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | } 4 | 5 | dependencies { 6 | implementation(kotlin("stdlib")) 7 | } -------------------------------------------------------------------------------- /lint/annotation/src/main/java/com/m3u/annotation/Exclude.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.annotation 2 | 3 | @Target(AnnotationTarget.FIELD) 4 | @Retention(AnnotationRetention.SOURCE) 5 | annotation class Exclude 6 | -------------------------------------------------------------------------------- /lint/annotation/src/main/java/com/m3u/annotation/Likable.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.annotation 2 | 3 | @Target(AnnotationTarget.CLASS) 4 | @Retention(AnnotationRetention.SOURCE) 5 | annotation class Likable 6 | -------------------------------------------------------------------------------- /lint/annotation/src/main/java/com/m3u/annotation/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.annotation 2 | 3 | 4 | fun interface Logger { 5 | fun log(obj: Any?) 6 | 7 | @Target(AnnotationTarget.CLASS) 8 | @Retention(AnnotationRetention.SOURCE) 9 | annotation class Generator( 10 | val name: String = "logger" 11 | ) 12 | } -------------------------------------------------------------------------------- /lint/annotation/src/main/java/com/m3u/annotation/MyDataClass.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.annotation 2 | 3 | @Target(AnnotationTarget.CLASS) 4 | @Retention(AnnotationRetention.SOURCE) 5 | annotation class MyDataClass( 6 | val name: String = "myCopy", 7 | val jvmOverload: Boolean = false 8 | ) { 9 | @Target(AnnotationTarget.FIELD) 10 | @Retention(AnnotationRetention.SOURCE) 11 | annotation class Exclude 12 | 13 | @Target(AnnotationTarget.FIELD) 14 | @Retention(AnnotationRetention.SOURCE) 15 | annotation class Include 16 | } 17 | -------------------------------------------------------------------------------- /lint/processor/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /lint/processor/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | alias(libs.plugins.com.google.devtools.ksp) 4 | } 5 | 6 | ksp { 7 | arg("autoserviceKsp.verify", "true") 8 | arg("autoserviceKsp.verbose", "true") 9 | } 10 | 11 | dependencies { 12 | implementation(project(":lint:annotation")) 13 | 14 | implementation(libs.symbol.processing.api) 15 | implementation(libs.kotlinpoet) 16 | implementation(libs.kotlinpoet.ksp) 17 | implementation(libs.auto.service.annotations) 18 | 19 | ksp(libs.auto.service.ksp) 20 | } -------------------------------------------------------------------------------- /lint/processor/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/lint/processor/consumer-rules.pro -------------------------------------------------------------------------------- /lint/processor/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /lint/processor/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lint/processor/src/main/java/com/m3u/processor/likable/LikableSymbolProcessorProvider.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.processor.likable 2 | 3 | import com.google.auto.service.AutoService 4 | import com.google.devtools.ksp.processing.SymbolProcessor 5 | import com.google.devtools.ksp.processing.SymbolProcessorEnvironment 6 | import com.google.devtools.ksp.processing.SymbolProcessorProvider 7 | 8 | @AutoService(SymbolProcessorProvider::class) 9 | class LikableSymbolProcessorProvider : SymbolProcessorProvider { 10 | override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { 11 | return LikableSymbolProcessor( 12 | logger = environment.logger, 13 | codeGenerator = environment.codeGenerator 14 | ) 15 | } 16 | } -------------------------------------------------------------------------------- /lint/processor/src/main/java/com/m3u/processor/likable/LoggerSymbolProcessorProvider.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.processor.likable 2 | 3 | import com.google.auto.service.AutoService 4 | import com.google.devtools.ksp.processing.SymbolProcessor 5 | import com.google.devtools.ksp.processing.SymbolProcessorEnvironment 6 | import com.google.devtools.ksp.processing.SymbolProcessorProvider 7 | 8 | @AutoService(SymbolProcessorProvider::class) 9 | class LoggerSymbolProcessorProvider : SymbolProcessorProvider { 10 | override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { 11 | return LoggerSymbolProcessor( 12 | codeGenerator = environment.codeGenerator, 13 | logger = environment.logger 14 | ) 15 | } 16 | } -------------------------------------------------------------------------------- /lint/processor/src/main/java/com/m3u/processor/likable/MyDataClassSymbolProcessorProvider.kt: -------------------------------------------------------------------------------- 1 | package com.m3u.processor.likable 2 | 3 | import com.google.auto.service.AutoService 4 | import com.google.devtools.ksp.processing.SymbolProcessor 5 | import com.google.devtools.ksp.processing.SymbolProcessorEnvironment 6 | import com.google.devtools.ksp.processing.SymbolProcessorProvider 7 | 8 | @AutoService(SymbolProcessorProvider::class) 9 | class MyDataClassSymbolProcessorProvider : SymbolProcessorProvider { 10 | override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { 11 | return MyDataClassSymbolProcessor( 12 | codeGenerator = environment.codeGenerator, 13 | logger = environment.logger 14 | ) 15 | } 16 | } -------------------------------------------------------------------------------- /play_store_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyroid/M3UAndroid/4617fcef87064b5d4a823ed3868413fba22ff7b7/play_store_512.png -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | maven("https://plugins.gradle.org/m2/") 7 | } 8 | } 9 | dependencyResolutionManagement { 10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 11 | repositories { 12 | google() 13 | mavenCentral() 14 | maven("https://jitpack.io") 15 | maven("https://plugins.gradle.org/m2/") 16 | } 17 | } 18 | rootProject.name = "M3U" 19 | include(":app:smartphone", ":app:tv", ":app:extension") 20 | include(":core", ":core:foundation", ":core:extension") 21 | include(":data") 22 | include( 23 | ":business:foryou", 24 | ":business:favorite", 25 | ":business:setting", 26 | ":business:playlist", 27 | ":business:playlist-configuration", 28 | ":business:channel", 29 | ":business:extension", 30 | ) 31 | include(":baselineprofile:smartphone", ":baselineprofile:tv") 32 | include(":i18n") 33 | include( 34 | ":lint:annotation", 35 | ":lint:processor" 36 | ) 37 | --------------------------------------------------------------------------------