diff --git a/Sora.xcodeproj/project.pbxproj b/Sora.xcodeproj/project.pbxproj index f1a25fa..ba4d147 100644 --- a/Sora.xcodeproj/project.pbxproj +++ b/Sora.xcodeproj/project.pbxproj @@ -7,307 +7,227 @@ objects = { /* Begin PBXBuildFile section */ - 1308CFBC2D19844A004CD38C /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1308CFBB2D19844A004CD38C /* Double+Extension.swift */; }; - 1308CFBE2D19844D004CD38C /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1308CFBD2D19844D004CD38C /* MusicProgressSlider.swift */; }; - 1308CFC12D198466004CD38C /* CustomPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1308CFC02D198466004CD38C /* CustomPlayer.swift */; }; - 132417842D13198000B4F2D2 /* SoraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417832D13198000B4F2D2 /* SoraApp.swift */; }; - 132417862D13198000B4F2D2 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417852D13198000B4F2D2 /* ContentView.swift */; }; - 132417882D13198200B4F2D2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 132417872D13198200B4F2D2 /* Assets.xcassets */; }; - 1324178B2D13198200B4F2D2 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1324178A2D13198200B4F2D2 /* Preview Assets.xcassets */; }; - 1324179E2D1319E800B4F2D2 /* MiruDataStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417932D1319E800B4F2D2 /* MiruDataStruct.swift */; }; - 1324179F2D1319E800B4F2D2 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417952D1319E800B4F2D2 /* Notification.swift */; }; - 132417A02D1319E800B4F2D2 /* HistoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417972D1319E800B4F2D2 /* HistoryManager.swift */; }; - 132417A12D1319E800B4F2D2 /* ModuleStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417992D1319E800B4F2D2 /* ModuleStruct.swift */; }; - 132417A22D1319E800B4F2D2 /* ModulesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1324179A2D1319E800B4F2D2 /* ModulesManager.swift */; }; - 132417A32D1319E800B4F2D2 /* NormalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1324179C2D1319E800B4F2D2 /* NormalPlayer.swift */; }; - 132417B82D131A0600B4F2D2 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417A72D131A0600B4F2D2 /* SearchView.swift */; }; - 132417B92D131A0600B4F2D2 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417A82D131A0600B4F2D2 /* SearchResultsView.swift */; }; - 132417BA2D131A0600B4F2D2 /* SettingsAboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417AB2D131A0600B4F2D2 /* SettingsAboutView.swift */; }; - 132417BB2D131A0600B4F2D2 /* SettingsIUView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417AC2D131A0600B4F2D2 /* SettingsIUView.swift */; }; - 132417BC2D131A0600B4F2D2 /* SettingsLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417AD2D131A0600B4F2D2 /* SettingsLogsView.swift */; }; - 132417BD2D131A0600B4F2D2 /* SettingsModuleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417AE2D131A0600B4F2D2 /* SettingsModuleView.swift */; }; - 132417BE2D131A0600B4F2D2 /* SettingsPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417AF2D131A0600B4F2D2 /* SettingsPlayerView.swift */; }; - 132417BF2D131A0600B4F2D2 /* SettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417B02D131A0600B4F2D2 /* SettingView.swift */; }; - 132417C02D131A0600B4F2D2 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417B12D131A0600B4F2D2 /* HomeView.swift */; }; - 132417C12D131A0600B4F2D2 /* LibraryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417B32D131A0600B4F2D2 /* LibraryManager.swift */; }; - 132417C22D131A0600B4F2D2 /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417B42D131A0600B4F2D2 /* LibraryView.swift */; }; - 132417C32D131A0600B4F2D2 /* MediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417B62D131A0600B4F2D2 /* MediaView.swift */; }; - 132417C42D131A0600B4F2D2 /* MediaExtraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417B72D131A0600B4F2D2 /* MediaExtraction.swift */; }; - 132417CF2D131B7400B4F2D2 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 132417CE2D131B7400B4F2D2 /* SwiftSoup */; }; - 132417D22D131C5300B4F2D2 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 132417D12D131C5300B4F2D2 /* Kingfisher */; }; - 132417D52D13240200B4F2D2 /* EpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417D42D13240200B4F2D2 /* EpisodeCell.swift */; }; - 132417D72D13242400B4F2D2 /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417D62D13242400B4F2D2 /* CircularProgressBar.swift */; }; - 132417D92D1328B900B4F2D2 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417D82D1328B900B4F2D2 /* VideoPlayerView.swift */; }; - 1352BA712D1ABC30000A9AF9 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1352BA702D1ABC30000A9AF9 /* URLSession.swift */; }; - 13AEE7BA2D2451F200CA634A /* GitHubAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13AEE7B92D2451F200CA634A /* GitHubAPI.swift */; }; - 13AEE7BC2D24521200CA634A /* SettingsReleasesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13AEE7BB2D24521200CA634A /* SettingsReleasesView.swift */; }; - 13B3A4B22D1477F100BCC0D5 /* SettingsStorageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13B3A4B12D1477F100BCC0D5 /* SettingsStorageView.swift */; }; - 13B544B02D26D8E900CC6C59 /* OpenCastSwift iOS in Frameworks */ = {isa = PBXBuildFile; productRef = 13B544AF2D26D8E900CC6C59 /* OpenCastSwift iOS */; }; - 13C9821F2D2152B1007A0132 /* GitHubRelease.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13C9821E2D2152B1007A0132 /* GitHubRelease.swift */; }; - 13ED65752D284045008F4C23 /* SettingsEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13ED65742D284045008F4C23 /* SettingsEditorView.swift */; }; + 133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6D2D2BE2500075467E /* SoraApp.swift */; }; + 133D7C702D2BE2500075467E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6F2D2BE2500075467E /* ContentView.swift */; }; + 133D7C722D2BE2520075467E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 133D7C712D2BE2520075467E /* Assets.xcassets */; }; + 133D7C752D2BE2520075467E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 133D7C742D2BE2520075467E /* Preview Assets.xcassets */; }; + 133D7C8C2D2BE2640075467E /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C7C2D2BE2630075467E /* SearchView.swift */; }; + 133D7C8D2D2BE2640075467E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C7D2D2BE2630075467E /* HomeView.swift */; }; + 133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C7E2D2BE2630075467E /* LibraryView.swift */; }; + 133D7C8F2D2BE2640075467E /* MediaInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C802D2BE2630075467E /* MediaInfoView.swift */; }; + 133D7C902D2BE2640075467E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C822D2BE2630075467E /* SettingsView.swift */; }; + 133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C842D2BE2630075467E /* SettingsViewModule.swift */; }; + 133D7C922D2BE2640075467E /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C872D2BE2640075467E /* URLSession.swift */; }; + 133D7C932D2BE2640075467E /* Modules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C892D2BE2640075467E /* Modules.swift */; }; + 133D7C942D2BE2640075467E /* JSController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C8B2D2BE2640075467E /* JSController.swift */; }; + 133D7C972D2BE2AF0075467E /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 133D7C962D2BE2AF0075467E /* Kingfisher */; }; + 138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */; }; + 138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */; }; + 13DC0C462D302C7500D0F966 /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DC0C452D302C7500D0F966 /* VideoPlayer.swift */; }; + 13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */; }; + 13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */; }; + 13EA2BD72D32D97400C1EBD7 /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD42D32D97400C1EBD7 /* MusicProgressSlider.swift */; }; + 13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */; }; + 13EA2BDC2D32D9FF00C1EBD7 /* MiruDataStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BDB2D32D9FF00C1EBD7 /* MiruDataStruct.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 1308CFBB2D19844A004CD38C /* Double+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; }; - 1308CFBD2D19844D004CD38C /* MusicProgressSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = ""; }; - 1308CFC02D198466004CD38C /* CustomPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPlayer.swift; sourceTree = ""; }; - 132417802D13198000B4F2D2 /* Sora.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sora.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 132417832D13198000B4F2D2 /* SoraApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraApp.swift; sourceTree = ""; }; - 132417852D13198000B4F2D2 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - 132417872D13198200B4F2D2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 1324178A2D13198200B4F2D2 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 132417932D1319E800B4F2D2 /* MiruDataStruct.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MiruDataStruct.swift; sourceTree = ""; }; - 132417952D1319E800B4F2D2 /* Notification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; - 132417972D1319E800B4F2D2 /* HistoryManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HistoryManager.swift; sourceTree = ""; }; - 132417992D1319E800B4F2D2 /* ModuleStruct.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModuleStruct.swift; sourceTree = ""; }; - 1324179A2D1319E800B4F2D2 /* ModulesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModulesManager.swift; sourceTree = ""; }; - 1324179C2D1319E800B4F2D2 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = ""; }; - 132417A72D131A0600B4F2D2 /* SearchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; - 132417A82D131A0600B4F2D2 /* SearchResultsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = ""; }; - 132417AB2D131A0600B4F2D2 /* SettingsAboutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAboutView.swift; sourceTree = ""; }; - 132417AC2D131A0600B4F2D2 /* SettingsIUView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsIUView.swift; sourceTree = ""; }; - 132417AD2D131A0600B4F2D2 /* SettingsLogsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsLogsView.swift; sourceTree = ""; }; - 132417AE2D131A0600B4F2D2 /* SettingsModuleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsModuleView.swift; sourceTree = ""; }; - 132417AF2D131A0600B4F2D2 /* SettingsPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsPlayerView.swift; sourceTree = ""; }; - 132417B02D131A0600B4F2D2 /* SettingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingView.swift; sourceTree = ""; }; - 132417B12D131A0600B4F2D2 /* HomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; - 132417B32D131A0600B4F2D2 /* LibraryManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryManager.swift; sourceTree = ""; }; - 132417B42D131A0600B4F2D2 /* LibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = ""; }; - 132417B62D131A0600B4F2D2 /* MediaView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = ""; }; - 132417B72D131A0600B4F2D2 /* MediaExtraction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaExtraction.swift; sourceTree = ""; }; - 132417C52D131AA500B4F2D2 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 132417D42D13240200B4F2D2 /* EpisodeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeCell.swift; sourceTree = ""; }; - 132417D62D13242400B4F2D2 /* CircularProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = ""; }; - 132417D82D1328B900B4F2D2 /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = ""; }; - 1352BA6F2D1AB113000A9AF9 /* Sora.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Sora.entitlements; sourceTree = ""; }; - 1352BA702D1ABC30000A9AF9 /* URLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSession.swift; sourceTree = ""; }; - 13AEE7B92D2451F200CA634A /* GitHubAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubAPI.swift; sourceTree = ""; }; - 13AEE7BB2D24521200CA634A /* SettingsReleasesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsReleasesView.swift; sourceTree = ""; }; - 13B3A4B12D1477F100BCC0D5 /* SettingsStorageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStorageView.swift; sourceTree = ""; }; - 13C9821E2D2152B1007A0132 /* GitHubRelease.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubRelease.swift; sourceTree = ""; }; - 13ED65742D284045008F4C23 /* SettingsEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsEditorView.swift; sourceTree = ""; }; + 133D7C6A2D2BE2500075467E /* Sora.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sora.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 133D7C6D2D2BE2500075467E /* SoraApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraApp.swift; sourceTree = ""; }; + 133D7C6F2D2BE2500075467E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 133D7C712D2BE2520075467E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 133D7C742D2BE2520075467E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 133D7C7C2D2BE2630075467E /* SearchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; + 133D7C7D2D2BE2630075467E /* HomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; + 133D7C7E2D2BE2630075467E /* LibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = ""; }; + 133D7C802D2BE2630075467E /* MediaInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaInfoView.swift; sourceTree = ""; }; + 133D7C822D2BE2630075467E /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + 133D7C842D2BE2630075467E /* SettingsViewModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewModule.swift; sourceTree = ""; }; + 133D7C872D2BE2640075467E /* URLSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSession.swift; sourceTree = ""; }; + 133D7C892D2BE2640075467E /* Modules.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Modules.swift; sourceTree = ""; }; + 133D7C8B2D2BE2640075467E /* JSController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSController.swift; sourceTree = ""; }; + 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeCell.swift; sourceTree = ""; }; + 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = ""; }; + 13DC0C412D2EC9BA00D0F966 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 13DC0C452D302C7500D0F966 /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; + 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPlayer.swift; sourceTree = ""; }; + 13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; }; + 13EA2BD42D32D97400C1EBD7 /* MusicProgressSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = ""; }; + 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = ""; }; + 13EA2BDB2D32D9FF00C1EBD7 /* MiruDataStruct.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MiruDataStruct.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 1324177D2D13198000B4F2D2 /* Frameworks */ = { + 133D7C672D2BE2500075467E /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 132417D22D131C5300B4F2D2 /* Kingfisher in Frameworks */, - 132417CF2D131B7400B4F2D2 /* SwiftSoup in Frameworks */, - 13B544B02D26D8E900CC6C59 /* OpenCastSwift iOS in Frameworks */, + 133D7C972D2BE2AF0075467E /* Kingfisher in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 1308CFBA2D19843E004CD38C /* CustomPlayer */ = { + 133D7C612D2BE2500075467E = { isa = PBXGroup; children = ( - 1308CFC02D198466004CD38C /* CustomPlayer.swift */, - 1308CFBF2D198450004CD38C /* Components */, - ); - path = CustomPlayer; - sourceTree = ""; - }; - 1308CFBF2D198450004CD38C /* Components */ = { - isa = PBXGroup; - children = ( - 1308CFBD2D19844D004CD38C /* MusicProgressSlider.swift */, - 1308CFBB2D19844A004CD38C /* Double+Extension.swift */, - ); - path = Components; - sourceTree = ""; - }; - 132417772D13198000B4F2D2 = { - isa = PBXGroup; - children = ( - 132417822D13198000B4F2D2 /* Sora */, - 132417812D13198000B4F2D2 /* Products */, + 133D7C6C2D2BE2500075467E /* Sora */, + 133D7C6B2D2BE2500075467E /* Products */, ); sourceTree = ""; }; - 132417812D13198000B4F2D2 /* Products */ = { + 133D7C6B2D2BE2500075467E /* Products */ = { isa = PBXGroup; children = ( - 132417802D13198000B4F2D2 /* Sora.app */, + 133D7C6A2D2BE2500075467E /* Sora.app */, ); name = Products; sourceTree = ""; }; - 132417822D13198000B4F2D2 /* Sora */ = { + 133D7C6C2D2BE2500075467E /* Sora */ = { isa = PBXGroup; children = ( - 1352BA6F2D1AB113000A9AF9 /* Sora.entitlements */, - 132417C52D131AA500B4F2D2 /* Info.plist */, - 132417912D1319E800B4F2D2 /* Utils */, - 132417A52D131A0600B4F2D2 /* Views */, - 132417832D13198000B4F2D2 /* SoraApp.swift */, - 132417852D13198000B4F2D2 /* ContentView.swift */, - 132417872D13198200B4F2D2 /* Assets.xcassets */, - 132417892D13198200B4F2D2 /* Preview Content */, + 13DC0C412D2EC9BA00D0F966 /* Info.plist */, + 13DC0C442D302C6A00D0F966 /* MediaPlayer */, + 133D7C852D2BE2640075467E /* Utils */, + 133D7C7B2D2BE2630075467E /* Views */, + 133D7C6D2D2BE2500075467E /* SoraApp.swift */, + 133D7C6F2D2BE2500075467E /* ContentView.swift */, + 133D7C712D2BE2520075467E /* Assets.xcassets */, + 133D7C732D2BE2520075467E /* Preview Content */, ); path = Sora; sourceTree = ""; }; - 132417892D13198200B4F2D2 /* Preview Content */ = { + 133D7C732D2BE2520075467E /* Preview Content */ = { isa = PBXGroup; children = ( - 1324178A2D13198200B4F2D2 /* Preview Assets.xcassets */, + 133D7C742D2BE2520075467E /* Preview Assets.xcassets */, ); path = "Preview Content"; sourceTree = ""; }; - 132417912D1319E800B4F2D2 /* Utils */ = { + 133D7C7B2D2BE2630075467E /* Views */ = { isa = PBXGroup; children = ( - 13C9821D2D2152A0007A0132 /* GitHub */, - 1308CFBA2D19843E004CD38C /* CustomPlayer */, - 132417922D1319E800B4F2D2 /* Miru */, - 132417942D1319E800B4F2D2 /* Extensions */, - 132417962D1319E800B4F2D2 /* History */, - 132417982D1319E800B4F2D2 /* Modules */, - 1324179B2D1319E800B4F2D2 /* Player */, - ); - path = Utils; - sourceTree = ""; - }; - 132417922D1319E800B4F2D2 /* Miru */ = { - isa = PBXGroup; - children = ( - 132417932D1319E800B4F2D2 /* MiruDataStruct.swift */, - ); - path = Miru; - sourceTree = ""; - }; - 132417942D1319E800B4F2D2 /* Extensions */ = { - isa = PBXGroup; - children = ( - 132417952D1319E800B4F2D2 /* Notification.swift */, - 1352BA702D1ABC30000A9AF9 /* URLSession.swift */, - ); - path = Extensions; - sourceTree = ""; - }; - 132417962D1319E800B4F2D2 /* History */ = { - isa = PBXGroup; - children = ( - 132417972D1319E800B4F2D2 /* HistoryManager.swift */, - ); - path = History; - sourceTree = ""; - }; - 132417982D1319E800B4F2D2 /* Modules */ = { - isa = PBXGroup; - children = ( - 132417992D1319E800B4F2D2 /* ModuleStruct.swift */, - 1324179A2D1319E800B4F2D2 /* ModulesManager.swift */, - ); - path = Modules; - sourceTree = ""; - }; - 1324179B2D1319E800B4F2D2 /* Player */ = { - isa = PBXGroup; - children = ( - 132417D82D1328B900B4F2D2 /* VideoPlayerView.swift */, - 1324179C2D1319E800B4F2D2 /* NormalPlayer.swift */, - ); - path = Player; - sourceTree = ""; - }; - 132417A52D131A0600B4F2D2 /* Views */ = { - isa = PBXGroup; - children = ( - 132417B12D131A0600B4F2D2 /* HomeView.swift */, - 132417B22D131A0600B4F2D2 /* LibraryViews */, - 132417A62D131A0600B4F2D2 /* SearchViews */, - 132417A92D131A0600B4F2D2 /* SettingsViews */, - 132417B52D131A0600B4F2D2 /* MediaViews */, + 133D7C832D2BE2630075467E /* SettingsSubViews */, + 133D7C7F2D2BE2630075467E /* MediaInfoView */, + 133D7C7D2D2BE2630075467E /* HomeView.swift */, + 133D7C7E2D2BE2630075467E /* LibraryView.swift */, + 133D7C7C2D2BE2630075467E /* SearchView.swift */, + 133D7C822D2BE2630075467E /* SettingsView.swift */, ); path = Views; sourceTree = ""; }; - 132417A62D131A0600B4F2D2 /* SearchViews */ = { + 133D7C7F2D2BE2630075467E /* MediaInfoView */ = { isa = PBXGroup; children = ( - 132417A72D131A0600B4F2D2 /* SearchView.swift */, - 132417A82D131A0600B4F2D2 /* SearchResultsView.swift */, + 138AA1B52D2D66EC0021F9DF /* EpisodeCell */, + 133D7C802D2BE2630075467E /* MediaInfoView.swift */, ); - path = SearchViews; + path = MediaInfoView; sourceTree = ""; }; - 132417A92D131A0600B4F2D2 /* SettingsViews */ = { + 133D7C832D2BE2630075467E /* SettingsSubViews */ = { isa = PBXGroup; children = ( - 132417AA2D131A0600B4F2D2 /* SubPages */, - 132417B02D131A0600B4F2D2 /* SettingView.swift */, + 133D7C842D2BE2630075467E /* SettingsViewModule.swift */, ); - path = SettingsViews; + path = SettingsSubViews; sourceTree = ""; }; - 132417AA2D131A0600B4F2D2 /* SubPages */ = { + 133D7C852D2BE2640075467E /* Utils */ = { isa = PBXGroup; children = ( - 132417AB2D131A0600B4F2D2 /* SettingsAboutView.swift */, - 132417AC2D131A0600B4F2D2 /* SettingsIUView.swift */, - 132417AD2D131A0600B4F2D2 /* SettingsLogsView.swift */, - 132417AE2D131A0600B4F2D2 /* SettingsModuleView.swift */, - 132417AF2D131A0600B4F2D2 /* SettingsPlayerView.swift */, - 13B3A4B12D1477F100BCC0D5 /* SettingsStorageView.swift */, - 13AEE7BB2D24521200CA634A /* SettingsReleasesView.swift */, - 13ED65742D284045008F4C23 /* SettingsEditorView.swift */, + 13EA2BDA2D32D9FF00C1EBD7 /* Miru */, + 133D7C862D2BE2640075467E /* Extensions */, + 133D7C882D2BE2640075467E /* Modules */, + 133D7C8A2D2BE2640075467E /* Loaders */, ); - path = SubPages; + path = Utils; sourceTree = ""; }; - 132417B22D131A0600B4F2D2 /* LibraryViews */ = { + 133D7C862D2BE2640075467E /* Extensions */ = { isa = PBXGroup; children = ( - 132417B42D131A0600B4F2D2 /* LibraryView.swift */, - 132417B32D131A0600B4F2D2 /* LibraryManager.swift */, + 133D7C872D2BE2640075467E /* URLSession.swift */, ); - path = LibraryViews; + path = Extensions; sourceTree = ""; }; - 132417B52D131A0600B4F2D2 /* MediaViews */ = { + 133D7C882D2BE2640075467E /* Modules */ = { isa = PBXGroup; children = ( - 132417D32D1323F500B4F2D2 /* EpisodesCell */, - 132417B62D131A0600B4F2D2 /* MediaView.swift */, - 132417B72D131A0600B4F2D2 /* MediaExtraction.swift */, + 133D7C892D2BE2640075467E /* Modules.swift */, ); - path = MediaViews; + path = Modules; sourceTree = ""; }; - 132417D32D1323F500B4F2D2 /* EpisodesCell */ = { + 133D7C8A2D2BE2640075467E /* Loaders */ = { isa = PBXGroup; children = ( - 132417D42D13240200B4F2D2 /* EpisodeCell.swift */, - 132417D62D13242400B4F2D2 /* CircularProgressBar.swift */, + 133D7C8B2D2BE2640075467E /* JSController.swift */, ); - path = EpisodesCell; + path = Loaders; sourceTree = ""; }; - 13C9821D2D2152A0007A0132 /* GitHub */ = { + 138AA1B52D2D66EC0021F9DF /* EpisodeCell */ = { isa = PBXGroup; children = ( - 13C9821E2D2152B1007A0132 /* GitHubRelease.swift */, - 13AEE7B92D2451F200CA634A /* GitHubAPI.swift */, + 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */, + 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */, ); - path = GitHub; + path = EpisodeCell; + sourceTree = ""; + }; + 13DC0C442D302C6A00D0F966 /* MediaPlayer */ = { + isa = PBXGroup; + children = ( + 13EA2BD02D32D97400C1EBD7 /* CustomPlayer */, + 13DC0C452D302C7500D0F966 /* VideoPlayer.swift */, + 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */, + ); + path = MediaPlayer; + sourceTree = ""; + }; + 13EA2BD02D32D97400C1EBD7 /* CustomPlayer */ = { + isa = PBXGroup; + children = ( + 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */, + 13EA2BD22D32D97400C1EBD7 /* Components */, + ); + path = CustomPlayer; + sourceTree = ""; + }; + 13EA2BD22D32D97400C1EBD7 /* Components */ = { + isa = PBXGroup; + children = ( + 13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */, + 13EA2BD42D32D97400C1EBD7 /* MusicProgressSlider.swift */, + ); + path = Components; + sourceTree = ""; + }; + 13EA2BDA2D32D9FF00C1EBD7 /* Miru */ = { + isa = PBXGroup; + children = ( + 13EA2BDB2D32D9FF00C1EBD7 /* MiruDataStruct.swift */, + ); + path = Miru; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 1324177F2D13198000B4F2D2 /* Sora */ = { + 133D7C692D2BE2500075467E /* Sora */ = { isa = PBXNativeTarget; - buildConfigurationList = 1324178E2D13198200B4F2D2 /* Build configuration list for PBXNativeTarget "Sora" */; + buildConfigurationList = 133D7C782D2BE2520075467E /* Build configuration list for PBXNativeTarget "Sora" */; buildPhases = ( - 1324177C2D13198000B4F2D2 /* Sources */, - 1324177D2D13198000B4F2D2 /* Frameworks */, - 1324177E2D13198000B4F2D2 /* Resources */, + 133D7C662D2BE2500075467E /* Sources */, + 133D7C672D2BE2500075467E /* Frameworks */, + 133D7C682D2BE2500075467E /* Resources */, ); buildRules = ( ); @@ -315,30 +235,28 @@ ); name = Sora; packageProductDependencies = ( - 132417CE2D131B7400B4F2D2 /* SwiftSoup */, - 132417D12D131C5300B4F2D2 /* Kingfisher */, - 13B544AF2D26D8E900CC6C59 /* OpenCastSwift iOS */, + 133D7C962D2BE2AF0075467E /* Kingfisher */, ); productName = Sora; - productReference = 132417802D13198000B4F2D2 /* Sora.app */; + productReference = 133D7C6A2D2BE2500075467E /* Sora.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ - 132417782D13198000B4F2D2 /* Project object */ = { + 133D7C622D2BE2500075467E /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1320; LastUpgradeCheck = 1320; TargetAttributes = { - 1324177F2D13198000B4F2D2 = { + 133D7C692D2BE2500075467E = { CreatedOnToolsVersion = 13.2.1; }; }; }; - buildConfigurationList = 1324177B2D13198000B4F2D2 /* Build configuration list for PBXProject "Sora" */; + buildConfigurationList = 133D7C652D2BE2500075467E /* Build configuration list for PBXProject "Sora" */; compatibilityVersion = "Xcode 13.0"; developmentRegion = en; hasScannedForEncodings = 0; @@ -346,78 +264,62 @@ en, Base, ); - mainGroup = 132417772D13198000B4F2D2; + mainGroup = 133D7C612D2BE2500075467E; packageReferences = ( - 132417CD2D131B7400B4F2D2 /* XCRemoteSwiftPackageReference "SwiftSoup" */, - 132417D02D131C5300B4F2D2 /* XCRemoteSwiftPackageReference "Kingfisher" */, - 13B544AE2D26D8E900CC6C59 /* XCRemoteSwiftPackageReference "OpenCastSwift" */, + 133D7C952D2BE2AF0075467E /* XCRemoteSwiftPackageReference "Kingfisher" */, ); - productRefGroup = 132417812D13198000B4F2D2 /* Products */; + productRefGroup = 133D7C6B2D2BE2500075467E /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( - 1324177F2D13198000B4F2D2 /* Sora */, + 133D7C692D2BE2500075467E /* Sora */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 1324177E2D13198000B4F2D2 /* Resources */ = { + 133D7C682D2BE2500075467E /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 1324178B2D13198200B4F2D2 /* Preview Assets.xcassets in Resources */, - 132417882D13198200B4F2D2 /* Assets.xcassets in Resources */, + 133D7C752D2BE2520075467E /* Preview Assets.xcassets in Resources */, + 133D7C722D2BE2520075467E /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 1324177C2D13198000B4F2D2 /* Sources */ = { + 133D7C662D2BE2500075467E /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 13B3A4B22D1477F100BCC0D5 /* SettingsStorageView.swift in Sources */, - 132417BB2D131A0600B4F2D2 /* SettingsIUView.swift in Sources */, - 132417C42D131A0600B4F2D2 /* MediaExtraction.swift in Sources */, - 132417B82D131A0600B4F2D2 /* SearchView.swift in Sources */, - 1308CFBC2D19844A004CD38C /* Double+Extension.swift in Sources */, - 132417D92D1328B900B4F2D2 /* VideoPlayerView.swift in Sources */, - 1324179F2D1319E800B4F2D2 /* Notification.swift in Sources */, - 132417BD2D131A0600B4F2D2 /* SettingsModuleView.swift in Sources */, - 132417BC2D131A0600B4F2D2 /* SettingsLogsView.swift in Sources */, - 1308CFC12D198466004CD38C /* CustomPlayer.swift in Sources */, - 132417A22D1319E800B4F2D2 /* ModulesManager.swift in Sources */, - 132417862D13198000B4F2D2 /* ContentView.swift in Sources */, - 13AEE7BA2D2451F200CA634A /* GitHubAPI.swift in Sources */, - 132417C22D131A0600B4F2D2 /* LibraryView.swift in Sources */, - 132417A32D1319E800B4F2D2 /* NormalPlayer.swift in Sources */, - 132417D72D13242400B4F2D2 /* CircularProgressBar.swift in Sources */, - 132417C02D131A0600B4F2D2 /* HomeView.swift in Sources */, - 132417BF2D131A0600B4F2D2 /* SettingView.swift in Sources */, - 132417C32D131A0600B4F2D2 /* MediaView.swift in Sources */, - 132417A12D1319E800B4F2D2 /* ModuleStruct.swift in Sources */, - 132417B92D131A0600B4F2D2 /* SearchResultsView.swift in Sources */, - 13AEE7BC2D24521200CA634A /* SettingsReleasesView.swift in Sources */, - 132417842D13198000B4F2D2 /* SoraApp.swift in Sources */, - 132417BE2D131A0600B4F2D2 /* SettingsPlayerView.swift in Sources */, - 132417C12D131A0600B4F2D2 /* LibraryManager.swift in Sources */, - 132417BA2D131A0600B4F2D2 /* SettingsAboutView.swift in Sources */, - 1324179E2D1319E800B4F2D2 /* MiruDataStruct.swift in Sources */, - 13ED65752D284045008F4C23 /* SettingsEditorView.swift in Sources */, - 1308CFBE2D19844D004CD38C /* MusicProgressSlider.swift in Sources */, - 132417D52D13240200B4F2D2 /* EpisodeCell.swift in Sources */, - 13C9821F2D2152B1007A0132 /* GitHubRelease.swift in Sources */, - 1352BA712D1ABC30000A9AF9 /* URLSession.swift in Sources */, - 132417A02D1319E800B4F2D2 /* HistoryManager.swift in Sources */, + 13DC0C462D302C7500D0F966 /* VideoPlayer.swift in Sources */, + 133D7C902D2BE2640075467E /* SettingsView.swift in Sources */, + 13EA2BD72D32D97400C1EBD7 /* MusicProgressSlider.swift in Sources */, + 13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */, + 133D7C932D2BE2640075467E /* Modules.swift in Sources */, + 133D7C702D2BE2500075467E /* ContentView.swift in Sources */, + 13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */, + 133D7C8F2D2BE2640075467E /* MediaInfoView.swift in Sources */, + 133D7C8D2D2BE2640075467E /* HomeView.swift in Sources */, + 13EA2BDC2D32D9FF00C1EBD7 /* MiruDataStruct.swift in Sources */, + 138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */, + 133D7C8C2D2BE2640075467E /* SearchView.swift in Sources */, + 133D7C942D2BE2640075467E /* JSController.swift in Sources */, + 133D7C922D2BE2640075467E /* URLSession.swift in Sources */, + 133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */, + 133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */, + 133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */, + 138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */, + 13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ - 1324178C2D13198200B4F2D2 /* Debug */ = { + 133D7C762D2BE2520075467E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -478,7 +380,7 @@ }; name = Debug; }; - 1324178D2D13198200B4F2D2 /* Release */ = { + 133D7C772D2BE2520075467E /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -533,12 +435,11 @@ }; name = Release; }; - 1324178F2D13198200B4F2D2 /* Debug */ = { + 133D7C792D2BE2520075467E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = Sora/Sora.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\""; @@ -556,22 +457,20 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.1.1; + MARKETING_VERSION = 0.2.0; PRODUCT_BUNDLE_IDENTIFIER = me.cranci.Sora; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; - 132417902D13198200B4F2D2 /* Release */ = { + 133D7C7A2D2BE2520075467E /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = Sora/Sora.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\""; @@ -589,10 +488,9 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.1.1; + MARKETING_VERSION = 0.2.0; PRODUCT_BUNDLE_IDENTIFIER = me.cranci.Sora; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -602,20 +500,20 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 1324177B2D13198000B4F2D2 /* Build configuration list for PBXProject "Sora" */ = { + 133D7C652D2BE2500075467E /* Build configuration list for PBXProject "Sora" */ = { isa = XCConfigurationList; buildConfigurations = ( - 1324178C2D13198200B4F2D2 /* Debug */, - 1324178D2D13198200B4F2D2 /* Release */, + 133D7C762D2BE2520075467E /* Debug */, + 133D7C772D2BE2520075467E /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 1324178E2D13198200B4F2D2 /* Build configuration list for PBXNativeTarget "Sora" */ = { + 133D7C782D2BE2520075467E /* Build configuration list for PBXNativeTarget "Sora" */ = { isa = XCConfigurationList; buildConfigurations = ( - 1324178F2D13198200B4F2D2 /* Debug */, - 132417902D13198200B4F2D2 /* Release */, + 133D7C792D2BE2520075467E /* Debug */, + 133D7C7A2D2BE2520075467E /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -623,15 +521,7 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 132417CD2D131B7400B4F2D2 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/scinfu/SwiftSoup.git"; - requirement = { - kind = exactVersion; - version = 2.4.0; - }; - }; - 132417D02D131C5300B4F2D2 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + 133D7C952D2BE2AF0075467E /* XCRemoteSwiftPackageReference "Kingfisher" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/onevcat/Kingfisher.git"; requirement = { @@ -639,33 +529,15 @@ version = 7.9.1; }; }; - 13B544AE2D26D8E900CC6C59 /* XCRemoteSwiftPackageReference "OpenCastSwift" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/danwilliams64/OpenCastSwift.git"; - requirement = { - branch = master; - kind = branch; - }; - }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 132417CE2D131B7400B4F2D2 /* SwiftSoup */ = { + 133D7C962D2BE2AF0075467E /* Kingfisher */ = { isa = XCSwiftPackageProductDependency; - package = 132417CD2D131B7400B4F2D2 /* XCRemoteSwiftPackageReference "SwiftSoup" */; - productName = SwiftSoup; - }; - 132417D12D131C5300B4F2D2 /* Kingfisher */ = { - isa = XCSwiftPackageProductDependency; - package = 132417D02D131C5300B4F2D2 /* XCRemoteSwiftPackageReference "Kingfisher" */; + package = 133D7C952D2BE2AF0075467E /* XCRemoteSwiftPackageReference "Kingfisher" */; productName = Kingfisher; }; - 13B544AF2D26D8E900CC6C59 /* OpenCastSwift iOS */ = { - isa = XCSwiftPackageProductDependency; - package = 13B544AE2D26D8E900CC6C59 /* XCRemoteSwiftPackageReference "OpenCastSwift" */; - productName = "OpenCastSwift iOS"; - }; /* End XCSwiftPackageProductDependency section */ }; - rootObject = 132417782D13198000B4F2D2 /* Project object */; + rootObject = 133D7C622D2BE2500075467E /* Project object */; } diff --git a/Sora.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Sora.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e6975d3..bdcfc53 100644 --- a/Sora.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Sora.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -9,42 +9,6 @@ "revision": "b6f62758f21a8c03cd64f4009c037cfa580a256e", "version": "7.9.1" } - }, - { - "package": "OpenCastSwift", - "repositoryURL": "https://github.com/danwilliams64/OpenCastSwift.git", - "state": { - "branch": "master", - "revision": "f96bf1ed9c1dcad34a1fcccb50d79a6cf612def4", - "version": null - } - }, - { - "package": "SwiftProtobuf", - "repositoryURL": "https://github.com/apple/swift-protobuf", - "state": { - "branch": null, - "revision": "9f0c76544701845ad98716f3f6a774a892152bcb", - "version": "1.26.0" - } - }, - { - "package": "SwiftSoup", - "repositoryURL": "https://github.com/scinfu/SwiftSoup.git", - "state": { - "branch": null, - "revision": "5386dab25134eec11fc35fc5e43caf422fad0270", - "version": "2.4.0" - } - }, - { - "package": "SwiftyJSON", - "repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON", - "state": { - "branch": null, - "revision": "af76cf3ef710b6ca5f8c05f3a31307d44a3c5828", - "version": "5.0.2" - } } ] }, diff --git a/Sora.xcodeproj/project.xcworkspace/xcuserdata/Francesco.xcuserdatad/UserInterfaceState.xcuserstate b/Sora.xcodeproj/project.xcworkspace/xcuserdata/Francesco.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index b0223cf..0000000 Binary files a/Sora.xcodeproj/project.xcworkspace/xcuserdata/Francesco.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ diff --git a/Sora.xcodeproj/xcuserdata/Francesco.xcuserdatad/xcschemes/xcschememanagement.plist b/Sora.xcodeproj/xcuserdata/Francesco.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index 6268ffc..0000000 --- a/Sora.xcodeproj/xcuserdata/Francesco.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,35 +0,0 @@ - - - - - SchemeUserState - - Playground (Playground) 1.xcscheme - - isShown - - orderHint - 2 - - Playground (Playground) 2.xcscheme - - isShown - - orderHint - 3 - - Playground (Playground).xcscheme - - isShown - - orderHint - 0 - - Sora.xcscheme_^#shared#^_ - - orderHint - 0 - - - - diff --git a/Sora/Assets.xcassets/AccentColor.colorset/.DS_Store b/Sora/Assets.xcassets/AccentColor.colorset/.DS_Store deleted file mode 100644 index 5008ddf..0000000 Binary files a/Sora/Assets.xcassets/AccentColor.colorset/.DS_Store and /dev/null differ diff --git a/Sora/ContentView.swift b/Sora/ContentView.swift index 58a8a13..01fca9c 100644 --- a/Sora/ContentView.swift +++ b/Sora/ContentView.swift @@ -2,14 +2,13 @@ // ContentView.swift // Sora // -// Created by Francesco on 18/12/24. +// Created by Francesco on 06/01/25. // import SwiftUI +import Kingfisher struct ContentView: View { - @EnvironmentObject var modulesManager: ModulesManager - var body: some View { TabView { HomeView() @@ -26,93 +25,8 @@ struct ContentView: View { } SettingsView() .tabItem { - Label("Settings", systemImage: "gearshape") + Label("Settings", systemImage: "gear") } } - .onAppear { - checkForUpdate() - Logger.shared.log("Started Sora") - } - } - - func checkForUpdate() { - fetchLatestRelease { release in - guard let release = release else { return } - - let latestVersion = release.tagName - let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.1.1" - - if latestVersion.compare(currentVersion, options: .numeric) == .orderedDescending { - DispatchQueue.main.async { - showUpdateAlert(release: release) - } - } - } - } - - func fetchLatestRelease(completion: @escaping (GitHubRelease?) -> Void) { - let url = URL(string: "https://api.github.com/repos/cranci1/Sora/releases/latest")! - - URLSession.custom.dataTask(with: url) { data, _, error in - guard let data = data, error == nil else { - completion(nil) - return - } - - let release = try? JSONDecoder().decode(GitHubRelease.self, from: data) - completion(release) - }.resume() - } - - func showUpdateAlert(release: GitHubRelease) { - let alert = UIAlertController(title: "Update Available", message: "A new version (\(release.tagName)) is available. Would you like to update Sora?", preferredStyle: .alert) - - alert.addAction(UIAlertAction(title: "Update", style: .default, handler: { _ in - self.showInstallOptionsAlert(release: release) - })) - - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) - - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootViewController = windowScene.windows.first?.rootViewController { - rootViewController.present(alert, animated: true, completion: nil) - } - } - - func showInstallOptionsAlert(release: GitHubRelease) { - let installAlert = UIAlertController(title: "Install Update", message: "Choose an installation method:", preferredStyle: .alert) - - let downloadUrl = release.assets.first?.browserDownloadUrl ?? "" - - installAlert.addAction(UIAlertAction(title: "Install in AltStore", style: .default, handler: { _ in - if let url = URL(string: "altstore://install?url=\(downloadUrl)") { - UIApplication.shared.open(url) - } - })) - - installAlert.addAction(UIAlertAction(title: "Install in Sidestore", style: .default, handler: { _ in - if let url = URL(string: "sidestore://install?url=\(downloadUrl)") { - UIApplication.shared.open(url) - } - })) - - installAlert.addAction(UIAlertAction(title: "Open in Safari", style: .default, handler: { _ in - if let url = URL(string: downloadUrl) { - UIApplication.shared.open(url) - } - })) - - installAlert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) - - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootViewController = windowScene.windows.first?.rootViewController { - rootViewController.present(installAlert, animated: true, completion: nil) - } - } -} - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() } } diff --git a/Sora/Info.plist b/Sora/Info.plist index 305d2ec..6a6654d 100644 --- a/Sora/Info.plist +++ b/Sora/Info.plist @@ -2,22 +2,10 @@ - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - me.cranci.scheme - CFBundleURLSchemes - - ryu - - - - UIBackgroundModes - - audio - + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + diff --git a/Sora/Utils/CustomPlayer/Components/Double+Extension.swift b/Sora/MediaPlayer/CustomPlayer/Components/Double+Extension.swift similarity index 100% rename from Sora/Utils/CustomPlayer/Components/Double+Extension.swift rename to Sora/MediaPlayer/CustomPlayer/Components/Double+Extension.swift diff --git a/Sora/Utils/CustomPlayer/Components/MusicProgressSlider.swift b/Sora/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift similarity index 100% rename from Sora/Utils/CustomPlayer/Components/MusicProgressSlider.swift rename to Sora/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift diff --git a/Sora/Utils/CustomPlayer/CustomPlayer.swift b/Sora/MediaPlayer/CustomPlayer/CustomPlayer.swift similarity index 98% rename from Sora/Utils/CustomPlayer/CustomPlayer.swift rename to Sora/MediaPlayer/CustomPlayer/CustomPlayer.swift index 2814aa4..79bbf00 100644 --- a/Sora/Utils/CustomPlayer/CustomPlayer.swift +++ b/Sora/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -36,20 +36,20 @@ struct CustomMediaPlayer: View { @State private var showWatchNextButton = true @Environment(\.presentationMode) var presentationMode - let module: ModuleStruct + let module: ScrapingModule let fullUrl: String let title: String let episodeNumber: Int let onWatchNext: () -> Void - init(module: ModuleStruct, urlString: String, fullUrl: String, title: String, episodeNumber: Int, onWatchNext: @escaping () -> Void) { + init(module: ScrapingModule, urlString: String, fullUrl: String, title: String, episodeNumber: Int, onWatchNext: @escaping () -> Void) { guard let url = URL(string: urlString) else { fatalError("Invalid URL string") } var request = URLRequest(url: url) if urlString.contains("ascdn") { - request.addValue("\(module.module[0].details.baseURL)", forHTTPHeaderField: "Referer") + request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer") } let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]]) diff --git a/Sora/Utils/Player/NormalPlayer.swift b/Sora/MediaPlayer/NormalPlayer.swift similarity index 100% rename from Sora/Utils/Player/NormalPlayer.swift rename to Sora/MediaPlayer/NormalPlayer.swift diff --git a/Sora/Utils/Player/VideoPlayerView.swift b/Sora/MediaPlayer/VideoPlayer.swift similarity index 91% rename from Sora/Utils/Player/VideoPlayerView.swift rename to Sora/MediaPlayer/VideoPlayer.swift index 98b388a..1d00cd9 100644 --- a/Sora/Utils/Player/VideoPlayerView.swift +++ b/Sora/MediaPlayer/VideoPlayer.swift @@ -1,23 +1,23 @@ // -// VideoPlayerView.swift +// VideoPlayer.swift // Sora // -// Created by Francesco on 18/12/24. +// Created by Francesco on 09/01/25. // import UIKit import AVKit class VideoPlayerViewController: UIViewController { - let module: ModuleStruct + let module: ScrapingModule var player: AVPlayer? - var playerViewController: AVPlayerViewController? + var playerViewController: NormalPlayer? var timeObserverToken: Any? var streamUrl: String? var fullUrl: String = "" - init(module: ModuleStruct) { + init(module: ScrapingModule) { self.module = module super.init(nibName: nil, bundle: nil) } @@ -35,7 +35,7 @@ class VideoPlayerViewController: UIViewController { var request = URLRequest(url: url) if streamUrl.contains("ascdn") { - request.addValue("\(module.module[0].details.baseURL)", forHTTPHeaderField: "Referer") + request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer") } let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]]) diff --git a/Sora/Sora.entitlements b/Sora/Sora.entitlements deleted file mode 100644 index ee95ab7..0000000 --- a/Sora/Sora.entitlements +++ /dev/null @@ -1,10 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.network.client - - - diff --git a/Sora/SoraApp.swift b/Sora/SoraApp.swift index bc0e9e6..d9adbf4 100644 --- a/Sora/SoraApp.swift +++ b/Sora/SoraApp.swift @@ -2,7 +2,7 @@ // SoraApp.swift // Sora // -// Created by Francesco on 18/12/24. +// Created by Francesco on 06/01/25. // import SwiftUI @@ -10,39 +10,17 @@ import SwiftUI @main struct SoraApp: App { @StateObject private var settings = Settings() - @StateObject private var modulesManager = ModulesManager() + @StateObject private var moduleManager = ModuleManager() var body: some Scene { WindowGroup { ContentView() + .environmentObject(moduleManager) .environmentObject(settings) - .environmentObject(modulesManager) .accentColor(settings.accentColor) .onAppear { settings.updateAppearance() } - .onOpenURL { url in - handleURL(url) - } - } - } - - private func handleURL(_ url: URL) { - guard url.scheme == "sora", - url.host == "module", - let components = URLComponents(url: url, resolvingAgainstBaseURL: true), - let moduleURL = components.queryItems?.first(where: { $0.name == "url" })?.value else { - return - } - - modulesManager.addModule(from: moduleURL) { result in - switch result { - case .success: - NotificationCenter.default.post(name: .moduleAdded, object: nil) - Logger.shared.log("Successfully added module from URL scheme: \(moduleURL)") - case .failure(let error): - Logger.shared.log("Failed to add module from URL scheme: \(error.localizedDescription)") - } } } } diff --git a/Sora/Utils/Extensions/Notification.swift b/Sora/Utils/Extensions/Notification.swift deleted file mode 100644 index 44863ae..0000000 --- a/Sora/Utils/Extensions/Notification.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// Notification.swift -// Sora -// -// Created by Francesco on 18/12/24. -// - -import Foundation - -extension Notification.Name { - static let moduleAdded = Notification.Name("moduleAdded") - static let moduleRemoved = Notification.Name("moduleRemoved") -} diff --git a/Sora/Utils/Extensions/URLSession.swift b/Sora/Utils/Extensions/URLSession.swift index 0ec1021..90fc6fa 100644 --- a/Sora/Utils/Extensions/URLSession.swift +++ b/Sora/Utils/Extensions/URLSession.swift @@ -1,17 +1,47 @@ // // URLSession.swift -// Sora +// Sora-JS // -// Created by Francesco on 24/12/24. +// Created by Francesco on 05/01/25. // import Foundation extension URLSession { + static let userAgents = [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.2365.92", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.2277.128", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.3; rv:123.0) Gecko/20100101 Firefox/123.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.2; rv:122.0) Gecko/20100101 Firefox/122.0", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0", + "Mozilla/5.0 (X11; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0", + "Mozilla/5.0 (Linux; Android 14; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.105 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.105 Mobile Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (iPad; CPU OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (Android 14; Mobile; rv:123.0) Gecko/123.0 Firefox/123.0", + "Mozilla/5.0 (Android 13; Mobile; rv:122.0) Gecko/122.0 Firefox/122.0" + ] + + static let randomUserAgent: String = { + userAgents.randomElement() ?? userAgents[0] + }() + static let custom: URLSession = { let configuration = URLSessionConfiguration.default configuration.httpAdditionalHeaders = [ - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + "User-Agent": randomUserAgent ] return URLSession(configuration: configuration) }() diff --git a/Sora/Utils/GitHub/GitHubAPI.swift b/Sora/Utils/GitHub/GitHubAPI.swift deleted file mode 100644 index 5dac7b5..0000000 --- a/Sora/Utils/GitHub/GitHubAPI.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// GitHubAPI.swift -// Sora -// -// Created by Francesco on 31/12/24. -// - -import Foundation - -struct GitHubReleases: Codable { - let tagName: String - let body: String - let htmlUrl: String - - enum CodingKeys: String, CodingKey { - case tagName = "tag_name" - case body - case htmlUrl = "html_url" - } -} - -class GitHubAPI { - static let shared = GitHubAPI() - - func fetchReleases(completion: @escaping ([GitHubReleases]?) -> Void) { - let url = URL(string: "https://api.github.com/repos/cranci1/Sora/releases")! - - URLSession.custom.dataTask(with: url) { data, _, error in - guard let data = data, error == nil else { - completion(nil) - return - } - - let releases = try? JSONDecoder().decode([GitHubReleases].self, from: data) - completion(releases) - }.resume() - } -} diff --git a/Sora/Utils/GitHub/GitHubRelease.swift b/Sora/Utils/GitHub/GitHubRelease.swift deleted file mode 100644 index 4803aa3..0000000 --- a/Sora/Utils/GitHub/GitHubRelease.swift +++ /dev/null @@ -1,124 +0,0 @@ -// -// GitHubRelease.swift -// Sora -// -// Created by Francesco on 29/12/24. -// - -import Foundation - -struct GitHubRelease: Codable { - let url: String - let assetsUrl: String - let uploadUrl: String - let htmlUrl: String - let id: Int - let author: Author - let nodeId: String - let tagName: String - let targetCommitish: String - let name: String - let draft: Bool - let prerelease: Bool - let createdAt: String - let publishedAt: String - let assets: [Asset] - let tarballUrl: String - let zipballUrl: String - let body: String - - enum CodingKeys: String, CodingKey { - case url - case assetsUrl = "assets_url" - case uploadUrl = "upload_url" - case htmlUrl = "html_url" - case id - case author - case nodeId = "node_id" - case tagName = "tag_name" - case targetCommitish = "target_commitish" - case name - case draft - case prerelease - case createdAt = "created_at" - case publishedAt = "published_at" - case assets - case tarballUrl = "tarball_url" - case zipballUrl = "zipball_url" - case body - } - - struct Author: Codable { - let login: String - let id: Int - let nodeId: String - let avatarUrl: String - let gravatarId: String - let url: String - let htmlUrl: String - let followersUrl: String - let followingUrl: String - let gistsUrl: String - let starredUrl: String - let subscriptionsUrl: String - let organizationsUrl: String - let reposUrl: String - let eventsUrl: String - let receivedEventsUrl: String - let type: String - let siteAdmin: Bool - - enum CodingKeys: String, CodingKey { - case login - case id - case nodeId = "node_id" - case avatarUrl = "avatar_url" - case gravatarId = "gravatar_id" - case url - case htmlUrl = "html_url" - case followersUrl = "followers_url" - case followingUrl = "following_url" - case gistsUrl = "gists_url" - case starredUrl = "starred_url" - case subscriptionsUrl = "subscriptions_url" - case organizationsUrl = "organizations_url" - case reposUrl = "repos_url" - case eventsUrl = "events_url" - case receivedEventsUrl = "received_events_url" - case type - case siteAdmin = "site_admin" - } - } - - struct Asset: Codable { - let url: String - let id: Int - let nodeId: String - let name: String - let label: String? - let uploader: Author - let contentType: String - let state: String - let size: Int - let downloadCount: Int - let createdAt: String - let updatedAt: String - let browserDownloadUrl: String - - enum CodingKeys: String, CodingKey { - case url - case id - case nodeId = "node_id" - case name - case label - case uploader - case contentType = "content_type" - case state - case size - case downloadCount = "download_count" - case createdAt = "created_at" - case updatedAt = "updated_at" - case browserDownloadUrl = "browser_download_url" - } - } -} diff --git a/Sora/Utils/History/HistoryManager.swift b/Sora/Utils/History/HistoryManager.swift deleted file mode 100644 index cbc7fe2..0000000 --- a/Sora/Utils/History/HistoryManager.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// HistoryManager.swift -// Sora -// -// Created by Francesco on 18/12/24. -// - -import Foundation -import Combine - -class HistoryManager: ObservableObject { - @Published var searchHistory: [String] = UserDefaults.standard.stringArray(forKey: "SearchHistory") ?? [] - - private var cancellables = Set() - - init() { - NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification) - .sink { [weak self] _ in - DispatchQueue.main.async { - self?.searchHistory = UserDefaults.standard.stringArray(forKey: "SearchHistory") ?? [] - } - } - .store(in: &cancellables) - } - - func addSearchHistory(_ item: String) { - if !searchHistory.contains(item) { - searchHistory.insert(item, at: 0) - UserDefaults.standard.set(searchHistory, forKey: "SearchHistory") - } - } - - func deleteHistoryItem(at offsets: IndexSet) { - searchHistory.remove(atOffsets: offsets) - UserDefaults.standard.set(searchHistory, forKey: "SearchHistory") - } -} diff --git a/Sora/Utils/Loaders/JSController.swift b/Sora/Utils/Loaders/JSController.swift new file mode 100644 index 0000000..a66e068 --- /dev/null +++ b/Sora/Utils/Loaders/JSController.swift @@ -0,0 +1,404 @@ +// +// JSController.swift +// Sora +// +// Created by Francesco on 05/01/25. +// + +import JavaScriptCore + +class JSController: ObservableObject { + private var context: JSContext + + init() { + self.context = JSContext() + setupContext() + } + + private func setupContext() { + let logFunction: @convention(block) (String) -> Void = { message in + print("JavaScript log: \(message)") + } + context.setObject(logFunction, forKeyedSubscript: "log" as NSString) + + let fetchNativeFunction: @convention(block) (String, JSValue, JSValue) -> Void = { urlString, resolve, reject in + guard let url = URL(string: urlString) else { + print("Invalid URL") + reject.call(withArguments: ["Invalid URL"]) + return + } + let task = URLSession.custom.dataTask(with: url) { data, _, error in + if let error = error { + print(url) + print("Network error in fetchNativeFunction: \(error.localizedDescription)") + reject.call(withArguments: [error.localizedDescription]) + return + } + guard let data = data else { + print("No data in response") + reject.call(withArguments: ["No data"]) + return + } + if let text = String(data: data, encoding: .utf8) { + resolve.call(withArguments: [text]) + } else { + print("Unable to decode data to text") + reject.call(withArguments: ["Unable to decode data"]) + } + } + task.resume() + } + context.setObject(fetchNativeFunction, forKeyedSubscript: "fetchNative" as NSString) + + let fetchDefinition = """ + function fetch(url) { + return new Promise(function(resolve, reject) { + fetchNative(url, resolve, reject); + }); + } + """ + context.evaluateScript(fetchDefinition) + } + + func loadScript(_ script: String) { + context = JSContext() + setupContext() + context.evaluateScript(script) + } + + func fetchSearchResults(keyword: String, module: ScrapingModule, completion: @escaping ([SearchItem]) -> Void) { + let searchUrl = module.metadata.searchBaseUrl.replacingOccurrences(of: "%s", with: keyword.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "") + + guard let url = URL(string: searchUrl) else { + completion([]) + return + } + + URLSession.custom.dataTask(with: url) { [weak self] data, _, error in + guard let self = self else { return } + + if let error = error { + print("Network error: \(error)") + DispatchQueue.main.async { completion([]) } + return + } + + guard let data = data, let html = String(data: data, encoding: .utf8) else { + print("Failed to decode HTML") + DispatchQueue.main.async { completion([]) } + return + } + + if let parseFunction = self.context.objectForKeyedSubscript("searchResults"), + let results = parseFunction.call(withArguments: [html]).toArray() as? [[String: String]] { + let resultItems = results.map { item in + SearchItem( + title: item["title"] ?? "", + imageUrl: item["image"] ?? "", + href: item["href"] ?? "" + ) + } + DispatchQueue.main.async { + completion(resultItems) + } + } else { + print("Failed to parse results") + DispatchQueue.main.async { completion([]) } + } + }.resume() + } + + func fetchDetails(url: String, completion: @escaping ([MediaItem], [EpisodeLink]) -> Void) { + guard let url = URL(string: url) else { + completion([], []) + return + } + + URLSession.custom.dataTask(with: url) { [weak self] data, _, error in + guard let self = self else { return } + + if let error = error { + print("Network error: \(error)") + DispatchQueue.main.async { completion([], []) } + return + } + + guard let data = data, let html = String(data: data, encoding: .utf8) else { + print("Failed to decode HTML") + DispatchQueue.main.async { completion([], []) } + return + } + + var resultItems: [MediaItem] = [] + var episodeLinks: [EpisodeLink] = [] + + if let parseFunction = self.context.objectForKeyedSubscript("extractDetails"), + let results = parseFunction.call(withArguments: [html]).toArray() as? [[String: String]] { + resultItems = results.map { item in + MediaItem( + description: item["description"] ?? "", + aliases: item["aliases"] ?? "", + airdate: item["airdate"] ?? "" + ) + } + } else { + print("Failed to parse results") + } + + if let fetchEpisodesFunction = self.context.objectForKeyedSubscript("extractEpisodes"), + let episodesResult = fetchEpisodesFunction.call(withArguments: [html]).toArray() as? [[String: String]] { + for episodeData in episodesResult { + if let num = episodeData["number"], let link = episodeData["href"], let number = Int(num) { + episodeLinks.append(EpisodeLink(number: number, href: link)) + } + } + } + + DispatchQueue.main.async { + completion(resultItems, episodeLinks) + } + }.resume() + } + + func fetchStreamUrl(episodeUrl: String, completion: @escaping (String?) -> Void) { + guard let url = URL(string: episodeUrl) else { + completion(nil) + return + } + + URLSession.custom.dataTask(with: url) { [weak self] data, _, error in + guard let self = self else { return } + + if let error = error { + print("Network error: \(error)") + DispatchQueue.main.async { completion(nil) } + return + } + + guard let data = data, let html = String(data: data, encoding: .utf8) else { + print("Failed to decode HTML") + DispatchQueue.main.async { completion(nil) } + return + } + + if let parseFunction = self.context.objectForKeyedSubscript("extractStreamUrl"), + let streamUrl = parseFunction.call(withArguments: [html]).toString() { + DispatchQueue.main.async { + completion(streamUrl) + } + } else { + print("Failed to extract stream URL") + DispatchQueue.main.async { completion(nil) } + } + }.resume() + } + + func fetchJsSearchResults(keyword: String, module: ScrapingModule, completion: @escaping ([SearchItem]) -> Void) { + if let exception = context.exception { + print("JavaScript exception: \(exception)") + completion([]) + return + } + + guard let searchResultsFunction = context.objectForKeyedSubscript("searchResults") else { + print("No JavaScript function searchResults found") + completion([]) + return + } + + let promiseValue = searchResultsFunction.call(withArguments: [keyword]) + guard let promise = promiseValue else { + print("searchResults did not return a Promise") + completion([]) + return + } + + let thenBlock: @convention(block) (JSValue) -> Void = { result in + + if let jsonString = result.toString(), + let data = jsonString.data(using: .utf8) { + do { + if let array = try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]] { + let resultItems = array.map { item -> SearchItem in + let title = item["title"] as? String ?? "" + let imageUrl = item["image"] as? String ?? "https://s4.anilist.co/file/anilistcdn/character/large/default.jpg" + let href = item["href"] as? String ?? "" + return SearchItem(title: title, imageUrl: imageUrl, href: href) + } + + DispatchQueue.main.async { + completion(resultItems) + } + + } else { + print("Failed to parse JSON") + DispatchQueue.main.async { + completion([]) + } + } + } catch { + print("JSON parsing error: \(error)") + DispatchQueue.main.async { + completion([]) + } + } + } else { + print("Result is not a string") + DispatchQueue.main.async { + completion([]) + } + } + } + + let catchBlock: @convention(block) (JSValue) -> Void = { error in + print("Promise rejected: \(String(describing: error.toString()))") + DispatchQueue.main.async { + completion([]) + } + } + + let thenFunction = JSValue(object: thenBlock, in: context) + let catchFunction = JSValue(object: catchBlock, in: context) + + promise.invokeMethod("then", withArguments: [thenFunction as Any]) + promise.invokeMethod("catch", withArguments: [catchFunction as Any]) + } + + func fetchDetailsJS(url: String, completion: @escaping ([MediaItem], [EpisodeLink]) -> Void) { + guard let url = URL(string: url) else { + completion([], []) + return + } + + if let exception = context.exception { + print("JavaScript exception: \(exception)") + completion([], []) + return + } + + guard let extractDetailsFunction = context.objectForKeyedSubscript("extractDetails") else { + print("No JavaScript function extractDetails found") + completion([], []) + return + } + + guard let extractEpisodesFunction = context.objectForKeyedSubscript("extractEpisodes") else { + print("No JavaScript function extractEpisodes found") + completion([], []) + return + } + + var resultItems: [MediaItem] = [] + var episodeLinks: [EpisodeLink] = [] + + let promiseValueDetails = extractDetailsFunction.call(withArguments: [url.absoluteString]) + guard let promiseDetails = promiseValueDetails else { + print("extractDetails did not return a Promise") + completion([], []) + return + } + + let thenBlockDetails: @convention(block) (JSValue) -> Void = { result in + + if let jsonOfDetails = result.toString(), + let dataDetails = jsonOfDetails.data(using: .utf8) { + do { + if let array = try JSONSerialization.jsonObject(with: dataDetails, options: []) as? [[String: Any]] { + resultItems = array.map { item -> MediaItem in + let description = item["description"] as? String ?? "" + let aliases = item["aliases"] as? String ?? "" + let airdate = item["airdate"] as? String ?? "" + return MediaItem(description: description, aliases: aliases, airdate: airdate) + } + } else { + print("Failed to parse JSON of extractDetails") + DispatchQueue.main.async { + completion([], []) + } + } + } catch { + print("JSON parsing error of extract details: \(error)") + DispatchQueue.main.async { + completion([], []) + } + } + } else { + print("Result is not a string of extractDetails") + DispatchQueue.main.async { + completion([], []) + } + } + } + + let catchBlockDetails: @convention(block) (JSValue) -> Void = { error in + print("Promise rejected of extractDetails: \(String(describing: error.toString()))") + DispatchQueue.main.async { + completion([], []) + } + } + + let thenFunctionDetails = JSValue(object: thenBlockDetails, in: context) + let catchFunctionDetails = JSValue(object: catchBlockDetails, in: context) + + promiseDetails.invokeMethod("then", withArguments: [thenFunctionDetails as Any]) + promiseDetails.invokeMethod("catch", withArguments: [catchFunctionDetails as Any]) + + + let promiseValueEpisodes = extractEpisodesFunction.call(withArguments: [url.absoluteString]) + guard let promiseEpisodes = promiseValueEpisodes else { + print("extractEpisodes did not return a Promise") + completion([], []) + return + } + + let thenBlockEpisodes: @convention(block) (JSValue) -> Void = { result in + + if let jsonOfEpisodes = result.toString(), + let dataEpisodes = jsonOfEpisodes.data(using: .utf8) { + do { + if let array = try JSONSerialization.jsonObject(with: dataEpisodes, options: []) as? [[String: Any]] { + episodeLinks = array.map { item -> EpisodeLink in + let number = item["number"] as? Int ?? 0 + let href = item["href"] as? String ?? "" + return EpisodeLink(number: number, href: href) + } + + DispatchQueue.main.async { + completion(resultItems, episodeLinks) + } + + } else { + print("Failed to parse JSON of extractEpisodes") + DispatchQueue.main.async { + completion([], []) + } + } + } catch { + print("JSON parsing error of extractEpisodes: \(error)") + DispatchQueue.main.async { + completion([], []) + } + } + } else { + print("Result is not a string of extractEpisodes") + DispatchQueue.main.async { + completion([], []) + } + } + } + + let catchBlockEpisodes: @convention(block) (JSValue) -> Void = { error in + print("Promise rejected of extractEpisodes: \(String(describing: error.toString()))") + DispatchQueue.main.async { + completion([], []) + } + } + + let thenFunctionEpisodes = JSValue(object: thenBlockEpisodes, in: context) + let catchFunctionEpisodes = JSValue(object: catchBlockEpisodes, in: context) + + promiseEpisodes.invokeMethod("then", withArguments: [thenFunctionEpisodes as Any]) + promiseEpisodes.invokeMethod("catch", withArguments: [catchFunctionEpisodes as Any]) + } + +} diff --git a/Sora/Utils/Modules/ModuleStruct.swift b/Sora/Utils/Modules/ModuleStruct.swift deleted file mode 100644 index c8725cc..0000000 --- a/Sora/Utils/Modules/ModuleStruct.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// ModuleStruct.swift -// Sora -// -// Created by Francesco on 18/12/24. -// - -import Foundation - -struct ModuleStruct: Codable { - let name: String - let version: String - let author: Author - let iconURL: String - let stream: String - let language: String - let extractor: String - let module: [Module] - - struct Author: Codable { - let name: String - let website: String - } - - struct Module: Codable, Hashable { - let search: Search - let featured: Featured - let details: Details - let episodes: Episodes - - struct Search: Codable, Hashable { - let url: String - let parameter: String - let documentSelector: String - let title: String - let image: Image - let href: String - let searchable: Bool? - - struct Image: Codable, Hashable { - let url: String - let attribute: String - } - } - - struct Featured: Codable, Hashable { - let url: String - let documentSelector: String - let title: String - let image: Image - let href: String - - struct Image: Codable, Hashable { - let url: String - let attribute: String - } - } - - struct Details: Codable, Hashable { - let baseURL: String - let pageRedirects: Bool? - let aliases: Aliases - let synopsis: String - let airdate: String - let stars: String - - struct Aliases: Codable, Hashable { - let selector: String - let attribute: String - } - } - - struct Episodes: Codable, Hashable { - let selector: String - let order: String - let pattern: String - let pattern2: String? - } - } -} diff --git a/Sora/Utils/Modules/Modules.swift b/Sora/Utils/Modules/Modules.swift new file mode 100644 index 0000000..d22a985 --- /dev/null +++ b/Sora/Utils/Modules/Modules.swift @@ -0,0 +1,120 @@ +// +// Modules.swift +// Sora +// +// Created by Francesco on 05/01/25. +// + +import Foundation + +struct ModuleMetadata: Codable, Hashable { + let sourceName: String + let author: String + let iconUrl: String + let version: String + let language: String + let baseUrl: String + let searchBaseUrl: String + let scriptUrl: String + let asyncJS: Bool? +} + +struct ScrapingModule: Codable, Identifiable, Hashable { + let id: UUID + let metadata: ModuleMetadata + let localPath: String + var isActive: Bool + + init(id: UUID = UUID(), metadata: ModuleMetadata, localPath: String, isActive: Bool = false) { + self.id = id + self.metadata = metadata + self.localPath = localPath + self.isActive = isActive + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + static func == (lhs: ScrapingModule, rhs: ScrapingModule) -> Bool { + lhs.id == rhs.id + } +} + +class ModuleManager: ObservableObject { + @Published var modules: [ScrapingModule] = [] + + private let fileManager = FileManager.default + private let modulesFileName = "modules.json" + + init() { + loadModules() + } + + private func getDocumentsDirectory() -> URL { + fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0] + } + + private func getModulesFilePath() -> URL { + getDocumentsDirectory().appendingPathComponent(modulesFileName) + } + + func loadModules() { + let url = getModulesFilePath() + guard let data = try? Data(contentsOf: url) else { return } + modules = (try? JSONDecoder().decode([ScrapingModule].self, from: data)) ?? [] + } + + private func saveModules() { + let url = getModulesFilePath() + guard let data = try? JSONEncoder().encode(modules) else { return } + try? data.write(to: url) + } + + func addModule(metadataUrl: String) async throws -> ScrapingModule { + guard let url = URL(string: metadataUrl) else { + throw NSError(domain: "Invalid metadata URL", code: -1) + } + + let (metadataData, _) = try await URLSession.custom.data(from: url) + let metadata = try JSONDecoder().decode(ModuleMetadata.self, from: metadataData) + + guard let scriptUrl = URL(string: metadata.scriptUrl) else { + throw NSError(domain: "Invalid script URL", code: -1) + } + + let (scriptData, _) = try await URLSession.custom.data(from: scriptUrl) + guard let jsContent = String(data: scriptData, encoding: .utf8) else { + throw NSError(domain: "Invalid script encoding", code: -1) + } + + let fileName = "\(UUID().uuidString).js" + let localUrl = getDocumentsDirectory().appendingPathComponent(fileName) + try jsContent.write(to: localUrl, atomically: true, encoding: .utf8) + + let module = ScrapingModule( + metadata: metadata, + localPath: fileName + ) + + DispatchQueue.main.async { + self.modules.append(module) + self.saveModules() + } + + return module + } + + func deleteModule(_ module: ScrapingModule) { + let localUrl = getDocumentsDirectory().appendingPathComponent(module.localPath) + try? fileManager.removeItem(at: localUrl) + + modules.removeAll { $0.id == module.id } + saveModules() + } + + func getModuleContent(_ module: ScrapingModule) throws -> String { + let localUrl = getDocumentsDirectory().appendingPathComponent(module.localPath) + return try String(contentsOf: localUrl, encoding: .utf8) + } +} diff --git a/Sora/Utils/Modules/ModulesManager.swift b/Sora/Utils/Modules/ModulesManager.swift deleted file mode 100644 index 99f9b3e..0000000 --- a/Sora/Utils/Modules/ModulesManager.swift +++ /dev/null @@ -1,155 +0,0 @@ -// -// ModulesManager.swift -// Sora -// -// Created by Francesco on 18/12/24. -// - -import Foundation - -class ModulesManager: ObservableObject { - @Published var modules: [ModuleStruct] = [] - @Published var isLoading = true - var moduleURLs: [String: String] = [:] - private let modulesFileName = "modules.json" - private let moduleURLsFileName = "moduleURLs.json" - - init() { - loadModules() - } - - func loadModules() { - isLoading = true - loadModuleURLs() - loadModuleData() - isLoading = false - } - - func addModule(from urlString: String, completion: @escaping (Result) -> Void) { - guard let url = URL(string: urlString) else { - completion(.failure(ModuleError.invalidURL)) - return - } - let task = URLSession.custom.dataTask(with: url) { data, _, error in - guard let data = data, error == nil else { - completion(.failure(error ?? ModuleError.unknown)) - return - } - do { - let module = try JSONDecoder().decode(ModuleStruct.self, from: data) - DispatchQueue.main.async { - if !self.modules.contains(where: { $0.name == module.name }) { - self.modules.append(module) - self.moduleURLs[module.name] = urlString - self.saveModuleData() - self.saveModuleURLs() - NotificationCenter.default.post(name: .moduleAdded, object: nil) - completion(.success(())) - } else { - completion(.failure(ModuleError.duplicateModule)) - } - } - } catch { - completion(.failure(error)) - } - } - task.resume() - } - - func deleteModule(named name: String) { - if let index = modules.firstIndex(where: { $0.name == name }) { - modules.remove(at: index) - moduleURLs.removeValue(forKey: name) - saveModuleData() - saveModuleURLs() - NotificationCenter.default.post(name: .moduleRemoved, object: nil) - } - } - - func refreshModules() { - for (name, urlString) in moduleURLs { - guard let url = URL(string: urlString) else { continue } - let task = URLSession.custom.dataTask(with: url) { data, _, error in - guard let data = data, error == nil else { return } - do { - let updatedModule = try JSONDecoder().decode(ModuleStruct.self, from: data) - DispatchQueue.main.async { - if let index = self.modules.firstIndex(where: { $0.name == name }) { - self.modules[index] = updatedModule - self.saveModuleData() - } - } - } catch { - print("Failed to decode module during refresh: \(error.localizedDescription)") - Logger.shared.log("Failed to decode module during refresh: \(error.localizedDescription)") - } - } - task.resume() - } - } - - private func loadModuleURLs() { - let fileURL = getDocumentsDirectory().appendingPathComponent(moduleURLsFileName) - do { - let data = try Data(contentsOf: fileURL) - moduleURLs = try JSONDecoder().decode([String: String].self, from: data) - } catch { - print("Failed to load module URLs: \(error.localizedDescription)") - Logger.shared.log("Failed to load module URLs: \(error.localizedDescription)") - } - } - - private func loadModuleData() { - let fileURL = getDocumentsDirectory().appendingPathComponent(modulesFileName) - do { - let data = try Data(contentsOf: fileURL) - modules = try JSONDecoder().decode([ModuleStruct].self, from: data) - } catch { - print("Failed to load modules: \(error.localizedDescription)") - Logger.shared.log("Failed to load modules: \(error.localizedDescription)") - } - } - - func saveModuleData() { - let fileURL = getDocumentsDirectory().appendingPathComponent(modulesFileName) - do { - let data = try JSONEncoder().encode(modules) - try data.write(to: fileURL) - } catch { - print("Failed to save modules: \(error.localizedDescription)") - Logger.shared.log("Failed to save modules: \(error.localizedDescription)") - } - } - - private func saveModuleURLs() { - let fileURL = getDocumentsDirectory().appendingPathComponent(moduleURLsFileName) - do { - let data = try JSONEncoder().encode(moduleURLs) - try data.write(to: fileURL) - } catch { - print("Failed to save module URLs: \(error.localizedDescription)") - Logger.shared.log("Failed to save module URLs: \(error.localizedDescription)") - } - } - - private func getDocumentsDirectory() -> URL { - FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] - } - - enum ModuleError: LocalizedError { - case invalidURL - case duplicateModule - case unknown - - var errorDescription: String? { - switch self { - case .invalidURL: - return "The provided URL is invalid." - case .duplicateModule: - return "This module already exists." - case .unknown: - return "An unknown error occurred." - } - } - } -} diff --git a/Sora/Views/HomeView.swift b/Sora/Views/HomeView.swift index 3af0df8..72ecb7c 100644 --- a/Sora/Views/HomeView.swift +++ b/Sora/Views/HomeView.swift @@ -2,145 +2,15 @@ // HomeView.swift // Sora // -// Created by Francesco on 18/12/24. +// Created by Francesco on 05/01/25. // import SwiftUI -import Kingfisher -import SwiftSoup struct HomeView: View { - @StateObject private var modulesManager = ModulesManager() - @State private var featuredItems: [String: [ItemResult]] = [:] - @State private var isLoading = true - var body: some View { - NavigationView { - ScrollView { - VStack(alignment: .leading) { - if isLoading { - ProgressView("Loading Featured Items...") - .padding() - } else { - ForEach(modulesManager.modules, id: \.name) { module in - if let items = featuredItems[module.name], !items.isEmpty { - VStack(alignment: .leading) { - HStack(alignment: .bottom) { - Text("Featured") - .font(.title2) - .bold() - .padding(.leading) - - Text("on \(module.name)") - .font(.system(size: 15)) - .foregroundColor(.secondary) - .bold() - } - - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 20) { - ForEach(items) { item in - NavigationLink(destination: MediaView(module: module, item: item)) { - VStack { - KFImage(URL(string: item.imageUrl)) - .resizable() - .scaledToFill() - .frame(width: 120, height: 180) - .clipped() - .cornerRadius(8) - - Text(item.name) - .font(.caption) - .lineLimit(1) - .foregroundColor(.primary) - } - .frame(width: 120) - .padding(.leading, 5) - } - } - } - .padding(.horizontal) - } - } - .padding(.bottom) - } - } - } - } - .navigationTitle("Home") - } - .onAppear { - if featuredItems.isEmpty { - fetchFeaturedItems() - } - } - } - .navigationViewStyle(StackNavigationViewStyle()) - } - - private func fetchFeaturedItems() { - isLoading = true - let group = DispatchGroup() - - for module in modulesManager.modules { - group.enter() - fetchFeaturedItems(for: module) { items in - DispatchQueue.main.async { - featuredItems[module.name] = items - group.leave() - } - } - } - - group.notify(queue: .main) { - isLoading = false - } - } - - private func fetchFeaturedItems(for module: ModuleStruct, completion: @escaping ([ItemResult]) -> Void) { - let urlString = module.module[0].featured.url - guard let url = URL(string: urlString) else { - completion([]) - return - } - - URLSession.custom.dataTask(with: url) { data, _, error in - guard let data = data, error == nil else { - completion([]) - return - } - - do { - let html = String(data: data, encoding: .utf8) ?? "" - let document = try SwiftSoup.parse(html) - let elements = try document.select(module.module[0].featured.documentSelector) - - var results: [ItemResult] = [] - for element in elements { - let title = try element.select(module.module[0].featured.title).text() - let href = try element.select(module.module[0].featured.href).attr("href") - var imageURL = try element.select(module.module[0].featured.image.url).attr(module.module[0].featured.image.attribute) - - if imageURL.contains(",") { - imageURL = imageURL.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.first ?? imageURL - } - - if !imageURL.starts(with: "http") { - imageURL = "\(module.module[0].details.baseURL.hasSuffix("/") ? module.module[0].details.baseURL : "\(module.module[0].details.baseURL)/")\(imageURL.hasPrefix("/") ? String(imageURL.dropFirst()) : imageURL)" - } - - imageURL = imageURL.replacingOccurrences(of: " ", with: "%20") - - let result = ItemResult(name: title, imageUrl: imageURL, href: href) - results.append(result) - } - - completion(results) - } catch { - print("Error parsing HTML: \(error)") - Logger.shared.log("Error parsing HTML: \(error)") - completion([]) - } - }.resume() + Text("Home View") + .font(.largeTitle) + .padding() } } diff --git a/Sora/Views/LibraryView.swift b/Sora/Views/LibraryView.swift new file mode 100644 index 0000000..11b7313 --- /dev/null +++ b/Sora/Views/LibraryView.swift @@ -0,0 +1,16 @@ +// +// LibraryView.swift +// Sora +// +// Created by Francesco on 05/01/25. +// + +import SwiftUI + +struct LibraryView: View { + var body: some View { + Text("Library View") + .font(.largeTitle) + .padding() + } +} diff --git a/Sora/Views/LibraryViews/LibraryManager.swift b/Sora/Views/LibraryViews/LibraryManager.swift deleted file mode 100644 index 4833d69..0000000 --- a/Sora/Views/LibraryViews/LibraryManager.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// LibraryManager.swift -// Sora -// -// Created by Francesco on 18/12/24. -// - -import Foundation - -class LibraryManager: ObservableObject { - static let shared = LibraryManager() - - @Published var libraryItems: [LibraryItem] = [] - private let userDefaults = UserDefaults.standard - private let libraryKey = "LibraryItems" - - private init() { - loadLibrary() - } - - func loadLibrary() { - if let data = userDefaults.data(forKey: libraryKey), - let decoded = try? JSONDecoder().decode([LibraryItem].self, from: data) { - libraryItems = decoded - } - } - - func saveLibrary() { - if let encoded = try? JSONEncoder().encode(libraryItems) { - userDefaults.set(encoded, forKey: libraryKey) - } - } - - func addToLibrary(_ item: LibraryItem) { - if !libraryItems.contains(where: { $0.id == item.id }) { - libraryItems.append(item) - saveLibrary() - Logger.shared.log("Added to library: \(item.title)") - } - } - - func removeFromLibrary(_ item: LibraryItem) { - libraryItems.removeAll(where: { $0.id == item.id }) - saveLibrary() - Logger.shared.log("Removed from library: \(item.title)") - } - - func importFromMiruData(_ miruData: MiruDataStruct, module: ModuleStruct) { - var newLibraryItems: [LibraryItem] = [] - - for like in miruData.likes { - let libraryItem = LibraryItem( - anilistID: like.anilistID, - title: like.title, - image: like.cover, - url: like.gogoSlug, - module: module, - dateAdded: Date() - ) - newLibraryItems.append(libraryItem) - Logger.shared.log("Importing item: \(libraryItem.title)") - } - - DispatchQueue.main.async { - self.libraryItems.append(contentsOf: newLibraryItems) - self.saveLibrary() - Logger.shared.log("Completed importing \(newLibraryItems.count) items") - } - } -} diff --git a/Sora/Views/LibraryViews/LibraryView.swift b/Sora/Views/LibraryViews/LibraryView.swift deleted file mode 100644 index 168eaff..0000000 --- a/Sora/Views/LibraryViews/LibraryView.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// LibraryView.swift -// Sora -// -// Created by Francesco on 18/12/24. -// - -import SwiftUI -import Kingfisher -import Foundation - -struct LibraryItem: Identifiable, Codable { - var id = UUID() - let anilistID: Int - let title: String - let image: String - let url: String - let module: ModuleStruct - var dateAdded: Date -} - -struct LibraryView: View { - @StateObject private var libraryManager = LibraryManager.shared - - var body: some View { - NavigationView { - ScrollView { - if libraryManager.libraryItems.isEmpty { - emptyLibraryView - } else { - LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))], spacing: 20) { - ForEach(libraryManager.libraryItems.sorted(by: { $0.dateAdded > $1.dateAdded })) { item in - NavigationLink(destination: MediaView(module: item.module, item: ItemResult(name: item.title, imageUrl: item.image, href: item.url))) { - itemView(item) - } - } - } - .padding() - } - } - .navigationTitle("Library") - } - .navigationViewStyle(StackNavigationViewStyle()) - } - - var emptyLibraryView: some View { - VStack(spacing: 8) { - Image(systemName: "books.vertical") - .font(.system(size: 75)) - .foregroundColor(.secondary) - Text("Your library is empty") - .font(.headline) - .multilineTextAlignment(.center) - Text("Start by adding items you find in the search results or by importing Miru bookmarks from settings!") - .font(.subheadline) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .position(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height / 3) - } - - func itemView(_ item: LibraryItem) -> some View { - VStack() { - ZStack(alignment: .bottomTrailing) { - KFImage(URL(string: item.image)) - .resizable() - .aspectRatio(2/3, contentMode: .fill) - .cornerRadius(10) - .frame(width: 150, height: 225) - - KFImage(URL(string: item.module.iconURL)) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 30, height: 30) - .background(Color.white) - .clipShape(Circle()) - .padding(5) - } - - Text(item.title) - .font(.subheadline) - .foregroundColor(Color.primary) - .padding([.leading, .bottom], 8) - .lineLimit(1) - } - .contextMenu { - Button(role: .destructive) { - libraryManager.removeFromLibrary(item) - } label: { - Label("Remove from Library", systemImage: "trash") - } - } - } -} diff --git a/Sora/Views/MediaViews/EpisodesCell/CircularProgressBar.swift b/Sora/Views/MediaInfoView/EpisodeCell/CircularProgressBar.swift similarity index 100% rename from Sora/Views/MediaViews/EpisodesCell/CircularProgressBar.swift rename to Sora/Views/MediaInfoView/EpisodeCell/CircularProgressBar.swift diff --git a/Sora/Views/MediaViews/EpisodesCell/EpisodeCell.swift b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift similarity index 84% rename from Sora/Views/MediaViews/EpisodesCell/EpisodeCell.swift rename to Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift index c384f49..d69506b 100644 --- a/Sora/Views/MediaViews/EpisodesCell/EpisodeCell.swift +++ b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift @@ -7,12 +7,16 @@ import SwiftUI import Kingfisher -import SwiftyJSON + +struct EpisodeLink: Identifiable { + let id = UUID() + let number: Int + let href: String +} struct EpisodeCell: View { let episode: String let episodeID: Int - let imageUrl: String let progress: Double let itemID: Int @@ -92,10 +96,12 @@ struct EpisodeCell: View { func parseEpisodeDetails(data: Data) { do { - let json = try JSON(data: data) - guard let episodeDetails = json["episodes"]["\(episodeID + 1)"].dictionary, - let title = episodeDetails["title"]?.dictionary, - let image = episodeDetails["image"]?.string else { + let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) + guard let json = jsonObject as? [String: Any], + let episodes = json["episodes"] as? [String: Any], + let episodeDetails = episodes["\(episodeID + 1)"] as? [String: Any], + let title = episodeDetails["title"] as? [String: String], + let image = episodeDetails["image"] as? String else { print("Invalid response format") DispatchQueue.main.async { self.isLoading = false @@ -104,7 +110,7 @@ struct EpisodeCell: View { } DispatchQueue.main.async { - self.episodeTitle = title["en"]?.string ?? "" + self.episodeTitle = title["en"] ?? "" self.episodeImageUrl = image self.isLoading = false } diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift new file mode 100644 index 0000000..b50c1af --- /dev/null +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -0,0 +1,323 @@ +// +// MediaInfoView.swift +// Sora +// +// Created by Francesco on 05/01/25. +// + +import SwiftUI +import Kingfisher +import SafariServices + +struct MediaItem: Identifiable { + let id = UUID() + let description: String + let aliases: String + let airdate: String +} + +struct MediaInfoView: View { + let title: String + let imageUrl: String + let href: String + let module: ScrapingModule + + @State var aliases: String = "" + @State var synopsis: String = "" + @State var airdate: String = "" + @State var episodeLinks: [EpisodeLink] = [] + @State var itemID: Int? + @State var isLoading: Bool = true + @State var showFullSynopsis: Bool = false + + @AppStorage("externalPlayer") private var externalPlayer: String = "Default" + + @StateObject private var jsController = JSController() + @EnvironmentObject var moduleManager: ModuleManager + + var body: some View { + Group { + if isLoading { + ProgressView() + .padding() + } else { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + HStack(alignment: .top, spacing: 10) { + KFImage(URL(string: imageUrl)) + .resizable() + .aspectRatio(2/3, contentMode: .fill) + .cornerRadius(10) + .frame(width: 150, height: 225) + + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.system(size: 17)) + .fontWeight(.bold) + + if !aliases.isEmpty && aliases != title { + Text(aliases) + .font(.system(size: 13)) + .foregroundColor(.secondary) + } + + Spacer() + + HStack(alignment: .center, spacing: 12) { + HStack(spacing: 4) { + Image(systemName: "calendar") + .resizable() + .frame(width: 15, height: 15) + .foregroundColor(.secondary) + + Text(airdate) + .font(.system(size: 12)) + .foregroundColor(.secondary) + } + .padding(4) + } + + HStack(alignment: .center, spacing: 12) { + Button(action: { + openSafariViewController(with: href) + }) { + HStack(spacing: 4) { + Text(module.metadata.sourceName) + .font(.system(size: 13)) + .foregroundColor(.primary) + + Image(systemName: "safari") + .resizable() + .frame(width: 20, height: 20) + .foregroundColor(.primary) + } + .padding(4) + .background(Capsule().fill(Color.accentColor.opacity(0.4))) + } + + Button(action: { + }) { + Image(systemName: "ellipsis.circle") + .resizable() + .frame(width: 20, height: 20) + } + } + } + } + + if !synopsis.isEmpty { + VStack(alignment: .leading, spacing: 2) { + HStack(alignment: .center) { + Text("Synopsis") + .font(.system(size: 18)) + .fontWeight(.bold) + + Spacer() + + Button(action: { + showFullSynopsis.toggle() + }) { + Text(showFullSynopsis ? "Less" : "More") + .font(.system(size: 14)) + } + } + + Text(synopsis) + .lineLimit(showFullSynopsis ? nil : 4) + .font(.system(size: 14)) + } + } + + HStack { + Button(action: { + }) { + HStack { + Image(systemName: "play.fill") + .foregroundColor(.primary) + Text("Start Watching") + .font(.headline) + .foregroundColor(.primary) + } + .padding() + .frame(maxWidth: .infinity) + .background(Color.accentColor) + .cornerRadius(10) + } + + Button(action: { + }) { + Image(systemName: "bookmark") + .resizable() + .frame(width: 20, height: 27) + } + } + + if !episodeLinks.isEmpty { + VStack(alignment: .leading, spacing: 10) { + Text("Episodes") + .font(.system(size: 18)) + .fontWeight(.bold) + + ForEach(episodeLinks.indices, id: \.self) { i in + let ep = episodeLinks[i] + let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)") + let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)") + let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0 + + EpisodeCell(episode: ep.href, episodeID: ep.number - 1, progress: progress, itemID: itemID ?? 0) + .onTapGesture { + fetchStream(href: ep.href) + } + } + } + } + } + .padding() + .navigationBarTitleDisplayMode(.inline) + .navigationBarTitle("") + .navigationViewStyle(StackNavigationViewStyle()) + } + } + } + .onAppear { + fetchDetails() + fetchItemID(byTitle: title) { result in + switch result { + case .success(let id): + itemID = id + case .failure(let error): + print("Failed to fetch Item ID: \(error)") + } + } + } + } + + func fetchDetails() { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + Task { + do { + let jsContent = try moduleManager.getModuleContent(module) + jsController.loadScript(jsContent) + if(module.metadata.asyncJS == false || module.metadata.asyncJS == nil) { + jsController.fetchDetails(url: href) { items, episodes in + if let item = items.first { + self.synopsis = item.description + self.aliases = item.aliases + self.airdate = item.airdate + } + self.episodeLinks = episodes + self.isLoading = false + } + } + else { + jsController.fetchDetailsJS(url: href) { items, episodes in + if let item = items.first { + self.synopsis = item.description + self.aliases = item.aliases + self.airdate = item.airdate + } + self.episodeLinks = episodes + self.isLoading = false + } + } + } catch { + print("Error loading module: \(error)") + self.isLoading = false + } + } + } + } + + func fetchStream(href: String) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + Task { + do { + let jsContent = try moduleManager.getModuleContent(module) + jsController.loadScript(jsContent) + jsController.fetchStreamUrl(episodeUrl: href) { streamUrl in + if let url = streamUrl { + playStream(url: url, fullURL: url) + } + } + } catch { + print("Error loading module: \(error)") + self.isLoading = false + } + } + } + } + + func playStream(url: String, fullURL: String) { + DispatchQueue.main.async { + let videoPlayerViewController = VideoPlayerViewController(module: module) + videoPlayerViewController.streamUrl = url + videoPlayerViewController.fullUrl = fullURL + videoPlayerViewController.modalPresentationStyle = .fullScreen + + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootVC = windowScene.windows.first?.rootViewController { + rootVC.present(videoPlayerViewController, animated: true, completion: nil) + } + } + } + + private func openSafariViewController(with urlString: String) { + guard let url = URL(string: urlString) else { + print("Unable to open the webpage") + return + } + let safariViewController = SFSafariViewController(url: url) + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootVC = windowScene.windows.first?.rootViewController { + rootVC.present(safariViewController, animated: true, completion: nil) + } + } + + private func fetchItemID(byTitle title: String, completion: @escaping (Result) -> Void) { + let query = """ + query { + Media(search: "\(title)", type: ANIME) { + id + } + } + """ + + guard let url = URL(string: "https://graphql.anilist.co") else { + completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]))) + return + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let parameters: [String: Any] = ["query": query] + request.httpBody = try? JSONSerialization.data(withJSONObject: parameters) + + URLSession.custom.dataTask(with: request) { data, _, error in + if let error = error { + completion(.failure(error)) + return + } + + guard let data = data else { + completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data received"]))) + return + } + + do { + if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], + let data = json["data"] as? [String: Any], + let media = data["Media"] as? [String: Any], + let id = media["id"] as? Int { + completion(.success(id)) + } else { + let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid response"]) + completion(.failure(error)) + } + } catch { + completion(.failure(error)) + } + }.resume() + } +} diff --git a/Sora/Views/MediaViews/MediaExtraction.swift b/Sora/Views/MediaViews/MediaExtraction.swift deleted file mode 100644 index d5d68a7..0000000 --- a/Sora/Views/MediaViews/MediaExtraction.swift +++ /dev/null @@ -1,407 +0,0 @@ -// -// MediaExtraction.swift -// Sora -// -// Created by Francesco on 18/12/24. -// - -import SwiftUI -import SwiftSoup - -extension MediaView { - func fetchItemDetails() { - guard !isFetchingStream else { return } - isFetchingStream = true - - guard let url = URL(string: item.href.hasPrefix("https") ? item.href : "\(module.module[0].details.baseURL.hasSuffix("/") ? module.module[0].details.baseURL : "\(module.module[0].details.baseURL)/")\(item.href.hasPrefix("/") ? String(item.href.dropFirst()) : item.href)") else { return } - - URLSession.custom.dataTask(with: url) { data, _, error in - defer { isLoading = false } - guard let data = data, error == nil else { return } - - do { - let html = String(data: data, encoding: .utf8) ?? "" - let document = try SwiftSoup.parse(html) - - let details = module.module[0].details - let episodes = module.module[0].episodes - - let aliases = (try? document.select(details.aliases.selector).attr(details.aliases.attribute)) ?? "" - let synopsis = (try? document.select(details.synopsis).text()) ?? "" - let airdate = (try? document.select(details.airdate).text()) ?? "" - let stars = (try? document.select(details.stars).text()) ?? "" - - let episodeElements = try document.select(episodes.selector) - var episodeList = (try? episodeElements.map { try $0.attr("href") }) ?? [] - - if module.module[0].episodes.order == "reversed" { - episodeList.reverse() - } - - DispatchQueue.main.async { - self.aliases = aliases - self.synopsis = synopsis - self.airdate = airdate - self.stars = stars - self.episodes = episodeList - } - } catch { - print("Error parsing HTML: \(error)") - Logger.shared.log("Error parsing HTML: \(error)") - } - }.resume() - isFetchingStream = false - } - - func fetchEpisodeStream(urlString: String) { - guard var url = URL(string: urlString.hasPrefix("https") ? urlString : "\(module.module[0].details.baseURL)\(urlString)") else { return } - - Logger.shared.log("Pressed episode button") - - let dispatchGroup = DispatchGroup() - - let pageRedirects = module.module[0].details.pageRedirects ?? false - - - if pageRedirects { - dispatchGroup.enter() // Start tracking the redirect task - URLSession.custom.dataTask(with: url) { data, _, error in - guard let data = data, error == nil else { - dispatchGroup.leave() // End tracking if there's an error - return - } - - let html = String(data: data, encoding: .utf8) ?? "" - let redirectedUrl = extractFromRedirectURL(from: html) - if let redirect = redirectedUrl, let newURL = URL(string: redirect) { - url = newURL - } - dispatchGroup.leave() // End tracking after successful execution - }.resume() - } - - dispatchGroup.notify(queue: .main) { // This block executes after all tasks - URLSession.custom.dataTask(with: url) { data, _, error in - guard let data = data, error == nil else { return } - - let html = String(data: data, encoding: .utf8) ?? "" - - - let streamType = module.stream - let streamURLs = extractStreamURLs(from: html, streamType: streamType) - - if module.extractor == "dub-sub" { - Logger.shared.log("extracting for dub-sub") - let dubSubURLs = extractDubSubURLs(from: html) - let subURLs = dubSubURLs.filter { $0.type == "SUB" }.map { $0.url } - let dubURLs = dubSubURLs.filter { $0.type == "DUB" }.map { $0.url } - - if !subURLs.isEmpty || !dubURLs.isEmpty { - DispatchQueue.main.async { - self.presentStreamSelection(subURLs: subURLs, dubURLs: dubURLs, fullURL: urlString) - } - } else { - DispatchQueue.main.async { - self.playStream(urlString: streamURLs.first, fullURL: urlString) - } - } - } else if module.extractor == "pattern-mp4" || module.extractor == "pattern-HLS" { - Logger.shared.log("extracting for pattern-mp4/hls") - let patternURL = extractPatternURL(from: html) - guard let patternURL = patternURL else { return } - - URLSession.custom.dataTask(with: patternURL) { data, _, error in - guard let data = data, error == nil else { return } - - let patternHTML = String(data: data, encoding: .utf8) ?? "" - let mp4URLs = extractStreamURLs(from: patternHTML, streamType: streamType).map { $0.replacingOccurrences(of: "amp;", with: "") } - - DispatchQueue.main.async { - self.playStream(urlString: mp4URLs.first, fullURL: urlString) - } - }.resume() - } else if module.extractor == "pattern" { - Logger.shared.log("extracting for pattern") - let patternURL = extractPatternURL(from: html) - - DispatchQueue.main.async { - self.playStream(urlString: patternURL?.absoluteString, fullURL: urlString) - } - } else if module.extractor == "voe" { - Logger.shared.log("extracting for voe") - - let voeUrl = extractVoeStream(from: html) - - DispatchQueue.main.async { - self.playStream(urlString: voeUrl?.absoluteString, fullURL: urlString) - } - - } else { - DispatchQueue.main.async { - self.playStream(urlString: streamURLs.first, fullURL: urlString) - } - } - }.resume() - } - } - - func extractStreamURLs(from html: String, streamType: String) -> [String] { - let pattern: String - switch streamType { - case "HLS": - pattern = #"https:\/\/[^"\s<>]+\.m3u8(?:\?[^\s"'<>]+)?"# - case "MP4": - pattern = #"https:\/\/[^"\s<>]+\.mp4(?:\?[^\s"'<>]+)?"# - default: - return [] - } - - do { - Logger.shared.log(streamType) - let regex = try NSRegularExpression(pattern: pattern, options: []) - let matches = regex.matches(in: html, options: [], range: NSRange(html.startIndex..., in: html)) - return matches.compactMap { - Range($0.range, in: html).map { String(html[$0]) } - } - } catch { - print("Invalid regex: \(error)") - Logger.shared.log("Invalid regex: \(error)") - return [] - } - } - - func extractPatternURL(from html: String) -> URL? { - var pattern = module.module[0].episodes.pattern - - if module.extractor == "pattern" { - if let data = Data(base64Encoded: pattern), let decodedPattern = String(data: data, encoding: .utf8) { - pattern = decodedPattern - } else { - print("Failed to decode base64 pattern") - Logger.shared.log("Failed to decode base64 pattern") - return nil - } - } - - do { - let regex = try NSRegularExpression(pattern: pattern, options: []) - let range = NSRange(html.startIndex.. [(type: String, url: String)] { - let pattern = #""type":"(SUB|DUB)","url":"(.*?\.m3u8)""# - - guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { - return [] - } - - let range = NSRange(htmlContent.startIndex..., in: htmlContent) - let matches = regex.matches(in: htmlContent, range: range) - - return matches.compactMap { match in - if match.numberOfRanges == 3, - let typeRange = Range(match.range(at: 1), in: htmlContent), - let urlRange = Range(match.range(at: 2), in: htmlContent) { - let type = String(htmlContent[typeRange]) - let urlString = String(htmlContent[urlRange]).replacingOccurrences(of: "\\/", with: "/") - Logger.shared.log(urlString) - return (type, urlString) - } - return nil - } - } - - /// Grabs hls stream from voe sites - func extractVoeStream(from html: String) -> URL? { - - let hlsPattern = "'hls': '(.*?)'" - guard let regex = try? NSRegularExpression(pattern: hlsPattern, options: []) else { return nil } - let range = NSRange(html.startIndex..., in: html) - if let match = regex.firstMatch(in: html, options: [], range: range), - let matchRange = Range(match.range(at: 1), in: html) { - let base64Hls = String(html[matchRange]) - guard let data = Data(base64Encoded: base64Hls), - let decodedURLString = String(data: data, encoding: .utf8) - else { return nil } - return URL(string: decodedURLString) - } - return nil - } - - - func presentStreamSelection(subURLs: [String], dubURLs: [String], fullURL: String) { - let uniqueSubURLs = Array(Set(subURLs)) - let uniqueDubURLs = Array(Set(dubURLs)) - - if uniqueSubURLs.count == 1 && uniqueDubURLs.isEmpty { - self.playStream(urlString: uniqueSubURLs.first, fullURL: fullURL) - return - } - - if uniqueDubURLs.count == 1 && uniqueSubURLs.isEmpty { - self.playStream(urlString: uniqueDubURLs.first, fullURL: fullURL) - return - } - - let alert = UIAlertController(title: "Select Stream", message: "Choose the audio type", preferredStyle: .actionSheet) - - if !uniqueDubURLs.isEmpty { - for dubURL in uniqueDubURLs { - alert.addAction(UIAlertAction(title: "DUB", style: .default) { _ in - self.playStream(urlString: dubURL, fullURL: fullURL) - }) - } - } - - if !uniqueSubURLs.isEmpty { - for subURL in uniqueSubURLs { - alert.addAction(UIAlertAction(title: "SUB", style: .default) { _ in - self.playStream(urlString: subURL, fullURL: fullURL) - }) - } - } - - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) - - DispatchQueue.main.async { - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootVC = windowScene.windows.first?.rootViewController { - if let popoverController = alert.popoverPresentationController { - popoverController.sourceView = rootVC.view - popoverController.sourceRect = CGRect(x: rootVC.view.bounds.midX, y: rootVC.view.bounds.midY, width: 0, height: 0) - popoverController.permittedArrowDirections = [] - } - rootVC.present(alert, animated: true, completion: nil) - } - } - } - - - /// Extracts the URL from a redirect page - /// Example: href="/redirect/1234567" -> https://baseUrl.com/redirect/1234567 - func extractFromRedirectURL(from html: String) -> String? { - - let pattern = #"href="\/redirect\/\d+""# - - do { - let regex = try NSRegularExpression(pattern: pattern, options: []) - let range = NSRange(html.startIndex.. String? { - let semaphore = DispatchSemaphore(value: 0) // To block the thread until the task completes - var redirectedURLString: String? - - var request = URLRequest(url: url) - request.httpMethod = "HEAD" // Use HEAD to get only headers - - let delegate = RedirectHandler() - let sessionConfig = URLSessionConfiguration.default - let session = URLSession(configuration: sessionConfig, delegate: delegate, delegateQueue: nil) - - session.dataTask(with: request) { _, response, error in - // Extract httpResponse as a standalone variable - guard let httpResponse = response as? HTTPURLResponse else { - Logger.shared.log("Invalid response for URL: \(url)") - semaphore.signal() - return - } - - // Process the httpResponse for redirection logic - if (httpResponse.statusCode == 301 || httpResponse.statusCode == 302), - let location = httpResponse.value(forHTTPHeaderField: "Location"), - let redirectedURL = URL(string: location) { - redirectedURLString = redirectedURL.absoluteString - Logger.shared.log("Redirected URL: \(redirectedURLString ?? "nil")") - } else { - if let error = error { - Logger.shared.log("Error fetching redirected URL: \(error.localizedDescription)") - } else { - Logger.shared.log("No redirection for URL: \(url)") - } - } - semaphore.signal() // Signal the semaphore to resume execution - }.resume() - - semaphore.wait() // Wait for the network task to complete - - if redirectedURLString?.contains("voe.sx") == true { - return voeUrlHandler(url: URL(string: redirectedURLString!)!) - } - else { - return redirectedURLString - } - - } - - /// Voe uses a custom handler to extract the video URL from the page - /// The site uses windows.location.href to redirect to the video page, usally another domain but with the same path - /// The replacement URL is hardcoded right now TODO: Make it dynamic - func voeUrlHandler(url: URL) -> String? { - - let urlString = url.absoluteString - - // Check if the URL is a voe.sx URL - guard urlString.contains("voe.sx") else { - Logger.shared.log("Not a voe.sx URL") - return nil - } - - // Extract the path from the URL and append it to the hardcoded replacement URL - // Example: https://voe.sx/e/1234567 -> /e/1234567 - let hardcodedURL = "https://sandratableother.com" - let finishedUrl = urlString.replacingOccurrences(of: "https://voe.sx", with: hardcodedURL) - - return finishedUrl - } - -} - -/// Custom handler to handle HTTP redirections and prevent them -class RedirectHandler: NSObject, URLSessionDelegate, URLSessionTaskDelegate { - func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping @Sendable (URLRequest?) -> Void - ) { - completionHandler(nil) - } -} diff --git a/Sora/Views/MediaViews/MediaView.swift b/Sora/Views/MediaViews/MediaView.swift deleted file mode 100644 index 7abe54f..0000000 --- a/Sora/Views/MediaViews/MediaView.swift +++ /dev/null @@ -1,377 +0,0 @@ -// -// MediaView.swift -// Sora -// -// Created by Francesco on 18/12/24. -// - -import AVKit -import SwiftUI -import Kingfisher -import SwiftyJSON -import SafariServices - -struct MediaView: View { - let module: ModuleStruct - let item: ItemResult - - @State var aliases: String = "" - @State var synopsis: String = "" - @State var airdate: String = "" - @State var stars: String = "" - @State var episodes: [String] = [] - @State var isLoading: Bool = true - @State var showFullSynopsis: Bool = false - @State var itemID: Int? - @State private var selectedEpisode: String = "" - @State private var selectedEpisodeNumber: Int = 0 - @State private var episodeRange: ClosedRange = 0...99 - @State var selectedRange: String = "1-100" - @State var isFetchingStream: Bool = false - - @AppStorage("externalPlayer") private var externalPlayer: String = "Default" - @StateObject private var libraryManager = LibraryManager.shared - - var body: some View { - Group { - if isLoading { - ProgressView() - .padding() - } else { - ScrollView { - VStack(alignment: .leading, spacing: 16) { - HStack(alignment: .top, spacing: 10) { - KFImage(URL(string: item.imageUrl)) - .resizable() - .aspectRatio(2/3, contentMode: .fill) - .cornerRadius(10) - .frame(width: 150, height: 225) - - VStack(alignment: .leading, spacing: 4) { - Text(item.name) - .font(.system(size: 17)) - .fontWeight(.bold) - - if !aliases.isEmpty && aliases != item.name { - Text(aliases) - .font(.system(size: 13)) - .foregroundColor(.secondary) - } - - Spacer() - - HStack(alignment: .center, spacing: 12) { - Text(module.name) - .font(.system(size: 13)) - .padding(4) - .background(Capsule().fill(Color.accentColor.opacity(0.4))) - - Button(action: { - }) { - Image(systemName: "ellipsis.circle") - .resizable() - .frame(width: 20, height: 20) - } - - Button(action: { - openSafariViewController(with: "\(module.module[0].details.baseURL)") - }) { - Image(systemName: "safari") - .resizable() - .frame(width: 20, height: 20) - } - } - } - } - - if !synopsis.isEmpty { - VStack(alignment: .leading, spacing: 2) { - HStack(alignment: .center) { - Text("Synopsis") - .font(.system(size: 18)) - .fontWeight(.bold) - - Spacer() - - Button(action: { - showFullSynopsis.toggle() - }) { - Text(showFullSynopsis ? "Less" : "More") - .font(.system(size: 14)) - } - } - - Text(synopsis) - .lineLimit(showFullSynopsis ? nil : 4) - .font(.system(size: 14)) - } - } - - HStack { - Button(action: { - startWatchingFirstUnfinishedEpisode() - }) { - HStack { - Image(systemName: "play.fill") - .foregroundColor(.primary) - Text("Start Watching") - .font(.headline) - .foregroundColor(.primary) - } - .padding() - .frame(maxWidth: .infinity) - .background(Color.accentColor) - .cornerRadius(10) - } - - Button(action: { - if isItemInLibrary() { - removeFromLibrary() - } else { - addToLibrary() - } - }) { - Image(systemName: isItemInLibrary() ? "bookmark.fill" : "bookmark") - .resizable() - .frame(width: 20, height: 27) - } - } - - if !episodes.isEmpty { - VStack(alignment: .leading, spacing: 10) { - HStack { - Text("Episodes") - .font(.system(size: 18)) - .fontWeight(.bold) - - Spacer() - - if episodes.count > 100 { - Menu { - ForEach(0..<(episodes.count / 100) + 1, id: \.self) { index in - let start = index * 100 + 1 - let end = min((index + 1) * 100, episodes.count) - Button(action: { - episodeRange = (start - 1)...(end - 1) - selectedRange = "\(start)-\(end)" - }) { - Text("\(start)-\(end)") - } - } - } label: { - Text(selectedRange) - .font(.system(size: 14)) - } - } - } - - ForEach(episodeRange, id: \.self) { index in - if index < episodes.count { - let episodeURL = episodes[index].hasPrefix("https") ? episodes[index] : "\(module.module[0].details.baseURL)\(episodes[index])" - let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(episodeURL)") - let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(episodeURL)") - let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0 - - EpisodeCell(episode: episodes[index], episodeID: index, imageUrl: item.imageUrl, progress: progress, itemID: itemID ?? 0) - .onTapGesture { - selectedEpisode = episodes[index] - selectedEpisodeNumber = index + 1 - fetchEpisodeStream(urlString: episodeURL) - } - } - } - } - } - } - .padding() - .navigationBarTitleDisplayMode(.inline) - .navigationBarTitle(item.name) - .navigationViewStyle(StackNavigationViewStyle()) - } - } - } - .onAppear { - fetchItemDetails() - fetchItemID(byTitle: item.name) { result in - switch result { - case .success(let id): - itemID = id - Logger.shared.log("Fetched Item ID: \(id)") - case .failure(let error): - print("Failed to fetch Item ID: \(error)") - Logger.shared.log("Failed to fetch Item ID: \(error)") - } - } - } - } - - func isItemInLibrary() -> Bool { - return libraryManager.libraryItems.contains(where: { $0.url == item.href }) - } - - func addToLibrary() { - let libraryItem = LibraryItem( - anilistID: itemID ?? 0, - title: item.name, - image: item.imageUrl, - url: item.href, - module: module, - dateAdded: Date() - ) - libraryManager.addToLibrary(libraryItem) - } - - func removeFromLibrary() { - if let libraryItem = libraryManager.libraryItems.first(where: { $0.url == item.href }) { - libraryManager.removeFromLibrary(libraryItem) - } - } - - private func startWatchingFirstUnfinishedEpisode() { - for (index, episode) in episodes.enumerated() { - let episodeURL = episode.hasPrefix("https") ? episode : "\(module.module[0].details.baseURL)\(episode)" - let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(episodeURL)") - let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(episodeURL)") - let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0 - - if progress < 0.90 { - selectedEpisode = episode - selectedEpisodeNumber = index + 1 - fetchEpisodeStream(urlString: episodeURL) - break - } - } - } - - func playStream(urlString: String?, fullURL: String) { - guard let streamUrl = urlString else { return } - - if externalPlayer == "Infuse" || externalPlayer == "VLC" || externalPlayer == "OutPlayer" || externalPlayer == "nPlayer" { - var scheme: String - switch externalPlayer { - case "Infuse": - scheme = "infuse://x-callback-url/play?url=" - case "VLC": - scheme = "vlc://" - case "OutPlayer": - scheme = "outplayer://" - case "nPlayer": - scheme = "nplayer-" - default: - scheme = "" - } - openInExternalPlayer(scheme: scheme, url: streamUrl) - Logger.shared.log("Opening external app with scheme: \(scheme)") - return - } else if externalPlayer == "Sora" { - DispatchQueue.main.async { - let customMediaPlayer = CustomMediaPlayer( - module: module, - urlString: streamUrl, - fullUrl: fullURL, - title: item.name, - episodeNumber: selectedEpisodeNumber, - onWatchNext: { - selectNextEpisode() - } - ) - let hostingController = UIHostingController(rootView: customMediaPlayer) - hostingController.modalPresentationStyle = .fullScreen - Logger.shared.log("Opening custom media player with url: \(streamUrl)") - - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootVC = windowScene.windows.first?.rootViewController { - rootVC.present(hostingController, animated: true, completion: nil) - } - } - return - } - - DispatchQueue.main.async { - let videoPlayerViewController = VideoPlayerViewController(module: module) - videoPlayerViewController.streamUrl = streamUrl - videoPlayerViewController.fullUrl = fullURL - videoPlayerViewController.modalPresentationStyle = .fullScreen - Logger.shared.log("Opening video player with url: \(streamUrl)") - - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootVC = windowScene.windows.first?.rootViewController { - rootVC.present(videoPlayerViewController, animated: true, completion: nil) - } - } - } - - private func selectNextEpisode() { - guard let currentEpisodeIndex = episodes.firstIndex(of: selectedEpisode) else { return } - let nextEpisodeIndex = currentEpisodeIndex + 1 - if nextEpisodeIndex < episodes.count { - selectedEpisode = episodes[nextEpisodeIndex] - selectedEpisodeNumber = nextEpisodeIndex + 1 - let nextEpisodeURL = "\(module.module[0].details.baseURL)\(episodes[nextEpisodeIndex])" - fetchEpisodeStream(urlString: nextEpisodeURL) - } - } - - private func openSafariViewController(with urlString: String) { - guard let url = URL(string: item.href.hasPrefix("https") ? item.href : "\(module.module[0].details.baseURL.hasSuffix("/") ? module.module[0].details.baseURL : "\(module.module[0].details.baseURL)/")\(item.href.hasPrefix("/") ? String(item.href.dropFirst()) : item.href)") else { - Logger.shared.log("Unable to open the webpage") - return - } - let safariViewController = SFSafariViewController(url: url) - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootVC = windowScene.windows.first?.rootViewController { - rootVC.present(safariViewController, animated: true, completion: nil) - } - } - - private func openInExternalPlayer(scheme: String, url: String) { - guard let streamUrl = URL(string: "\(scheme)\(url)") else { - Logger.shared.log("Unable to open the stream: '\(scheme)\(url)'") - return - } - UIApplication.shared.open(streamUrl, options: [:], completionHandler: nil) - Logger.shared.log("Unable to open the stream: 'streamUrl'") - } - - func fetchItemID(byTitle title: String, completion: @escaping (Result) -> Void) { - let query = """ - query { - Media(search: "\(title)", type: ANIME) { - id - } - } - """ - - guard let url = URL(string: "https://graphql.anilist.co") else { - completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]))) - return - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - - let parameters: [String: Any] = ["query": query] - request.httpBody = try? JSONSerialization.data(withJSONObject: parameters) - - URLSession.custom.dataTask(with: request) { data, _, error in - if let error = error { - completion(.failure(error)) - return - } - - guard let data = data else { - completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data received"]))) - return - } - - let json = JSON(data) - if let id = json["data"]["Media"]["id"].int { - completion(.success(id)) - } else { - let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid response"]) - completion(.failure(error)) - } - }.resume() - } -} diff --git a/Sora/Views/SearchView.swift b/Sora/Views/SearchView.swift new file mode 100644 index 0000000..8a0280d --- /dev/null +++ b/Sora/Views/SearchView.swift @@ -0,0 +1,211 @@ +// +// SearchView.swift +// Sora +// +// Created by Francesco on 05/01/25. +// + +import SwiftUI +import Kingfisher + +struct SearchItem: Identifiable { + let id = UUID() + let title: String + let imageUrl: String + let href: String +} + +struct SearchView: View { + @AppStorage("selectedModuleId") private var selectedModuleId: String? + @StateObject private var jsController = JSController() + @EnvironmentObject var moduleManager: ModuleManager + + @State private var searchItems: [SearchItem] = [] + @State private var selectedSearchItem: SearchItem? + @State private var isSearching = false + @State private var searchText = "" + @State private var hasNoResults = false + + private var selectedModule: ScrapingModule? { + guard let id = selectedModuleId else { return nil } + return moduleManager.modules.first { $0.id.uuidString == id } + } + + var body: some View { + NavigationView { + ScrollView { + VStack(spacing: 0) { + SearchBar(text: $searchText, onSearchButtonClicked: performSearch) + .padding() + .disabled(selectedModule == nil) + + if selectedModule == nil { + VStack(spacing: 8) { + Text("No Module Selected") + .font(.headline) + Text("Please select a module from settings") + .font(.caption) + .foregroundColor(.secondary) + } + .padding() + .frame(maxWidth: .infinity) + .background(Color(.systemBackground)) + .shadow(color: Color.black.opacity(0.1), radius: 2, y: 1) + } + + if isSearching { + ProgressView() + .padding() + } else if hasNoResults { + VStack(spacing: 8) { + Image(systemName: "magnifyingglass") + .font(.largeTitle) + .foregroundColor(.secondary) + Text("No Results Found") + .font(.headline) + Text("Try different keywords/titles") + .font(.caption) + .foregroundColor(.secondary) + } + .padding() + .frame(maxWidth: .infinity) + } + + LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))], spacing: 16) { + ForEach(searchItems) { item in + NavigationLink(destination: MediaInfoView(title: item.title, imageUrl: item.imageUrl, href: item.href, module: selectedModule!)) { + VStack { + KFImage(URL(string: item.imageUrl)) + .resizable() + .aspectRatio(2/3, contentMode: .fill) + .cornerRadius(10) + .frame(width: 150, height: 225) + + Text(item.title) + .font(.subheadline) + .foregroundColor(Color.primary) + .padding([.leading, .bottom], 8) + .lineLimit(1) + } + } + } + } + .padding() + } + } + .navigationTitle("Search") + .navigationBarTitleDisplayMode(.large) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + HStack { + if let selectedModule = selectedModule { + Text(selectedModule.metadata.sourceName) + .font(.headline) + .foregroundColor(.secondary) + } + Menu { + ForEach(moduleManager.modules) { module in + Button { + selectedModuleId = module.id.uuidString + } label: { + HStack { + KFImage(URL(string: module.metadata.iconUrl)) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + .cornerRadius(4) + + Text(module.metadata.sourceName) + Spacer() + if module.id.uuidString == selectedModuleId { + Image(systemName: "checkmark") + } + } + } + } + } label: { + Image(systemName: "chevron.up.chevron.down") + .foregroundColor(.secondary) + } + } + } + } + } + .navigationViewStyle(StackNavigationViewStyle()) + .onChange(of: selectedModuleId) { _ in + if !searchText.isEmpty { + performSearch() + } + } + } + + private func performSearch() { + guard !searchText.isEmpty, let module = selectedModule else { + searchItems = [] + hasNoResults = false + return + } + + isSearching = true + hasNoResults = false + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + Task { + do { + let jsContent = try moduleManager.getModuleContent(module) + jsController.loadScript(jsContent) + if(module.metadata.asyncJS == false || module.metadata.asyncJS == nil) { + jsController.fetchSearchResults(keyword: searchText, module: module) { items in + searchItems = items + hasNoResults = items.isEmpty + isSearching = false + } + } else { + jsController.fetchJsSearchResults(keyword: searchText, module: module) { items in + searchItems = items + hasNoResults = items.isEmpty + isSearching = false + } + } + } catch { + print("Error loading module: \(error)") + isSearching = false + hasNoResults = true + } + } + } + } +} + +struct SearchBar: View { + @Binding var text: String + var onSearchButtonClicked: () -> Void + + var body: some View { + HStack { + TextField("Search...", text: $text, onCommit: onSearchButtonClicked) + .padding(7) + .padding(.horizontal, 25) + .background(Color(.systemGray6)) + .cornerRadius(8) + .overlay( + HStack { + Image(systemName: "magnifyingglass") + .foregroundColor(.secondary) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + .padding(.leading, 8) + + if !text.isEmpty { + Button(action: { + self.text = "" + }) { + Image(systemName: "multiply.circle.fill") + .foregroundColor(.secondary) + .padding(.trailing, 8) + } + } + } + ) + } + } +} diff --git a/Sora/Views/SearchViews/SearchResultsView.swift b/Sora/Views/SearchViews/SearchResultsView.swift deleted file mode 100644 index 0ce4eef..0000000 --- a/Sora/Views/SearchViews/SearchResultsView.swift +++ /dev/null @@ -1,236 +0,0 @@ -// -// SearchResultsView.swift -// Sora -// -// Created by Francesco on 18/12/24. -// - -import SwiftUI -import SwiftSoup -import Kingfisher - -struct SearchResultsView: View { - let module: ModuleStruct? - let searchText: String - @State private var searchResults: [ItemResult] = [] - @State private var isLoading: Bool = true - @State private var filter: FilterType = .all - @AppStorage("listSearch") private var isListSearchEnabled: Bool = false - - enum FilterType: String, CaseIterable { - case all = "All" - case dub = "Dub" - case sub = "Sub" - case ova = "OVA" - case ona = "ONA" - case movie = "Movie" - } - - var body: some View { - if isListSearchEnabled { - oldUI - } else { - modernUI - } - } - - var modernUI: some View { - VStack { - if isLoading { - ProgressView() - .padding() - } else if searchResults.isEmpty { - Text("No results found") - .foregroundColor(.secondary) - .padding() - } else { - ScrollView { - LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))], spacing: 20) { - ForEach(filteredResults) { result in - NavigationLink(destination: MediaView(module: module!, item: result)) { - VStack { - KFImage(URL(string: result.imageUrl)) - .resizable() - .aspectRatio(2/3, contentMode: .fill) - .cornerRadius(10) - .frame(width: 150, height: 225) - - Text(result.name) - .font(.subheadline) - .foregroundColor(Color.primary) - .padding([.leading, .bottom], 8) - .lineLimit(1) - } - } - } - } - .padding() - } - .navigationViewStyle(StackNavigationViewStyle()) - .navigationTitle("Results") - .toolbar { - filterMenu - } - } - } - .onAppear { - performSearch() - } - } - - var oldUI: some View { - VStack { - if isLoading { - ProgressView() - .padding() - } else if searchResults.isEmpty { - Text("No results found") - .foregroundColor(.secondary) - .padding() - } else { - List { - ForEach(filteredResults) { result in - NavigationLink(destination: MediaView(module: module!, item: result)) { - HStack { - KFImage(URL(string: result.imageUrl)) - .resizable() - .scaledToFill() - .frame(width: 100, height: 150) - .clipped() - - VStack(alignment: .leading) { - Text(result.name) - .font(.system(size: 16)) - .padding(.leading, 10) - } - } - .padding(.vertical, 5) - } - } - } - .navigationTitle("Results") - .toolbar { - filterMenu - } - } - } - .navigationViewStyle(StackNavigationViewStyle()) - .onAppear { - performSearch() - } - } - - var filterMenu: some View { - Menu { - ForEach([FilterType.all], id: \.self) { filter in - Button(action: { - self.filter = filter - performSearch() - }) { - Label(filter.rawValue, systemImage: self.filter == filter ? "checkmark" : "") - } - } - Menu("Audio") { - ForEach([FilterType.dub, FilterType.sub], id: \.self) { filter in - Button(action: { - self.filter = filter - performSearch() - }) { - Label(filter.rawValue, systemImage: self.filter == filter ? "checkmark" : "") - } - } - } - Menu("Format") { - ForEach([FilterType.ova, FilterType.ona, FilterType.movie], id: \.self) { filter in - Button(action: { - self.filter = filter - performSearch() - }) { - Label(filter.rawValue, systemImage: self.filter == filter ? "checkmark" : "") - } - } - } - } label: { - Label("Filter", systemImage: filter == .all ? "line.horizontal.3.decrease.circle" : "line.horizontal.3.decrease.circle.fill") - } - } - - var filteredResults: [ItemResult] { - switch filter { - case .all: - return searchResults - case .dub: - return searchResults.filter { $0.name.contains("Dub") || $0.name.contains("ITA") } - case .sub: - return searchResults.filter { !$0.name.contains("Dub") && !$0.name.contains("ITA") } - case .ova, .ona: - return searchResults.filter { $0.name.contains(filter.rawValue) } - case .movie: - return searchResults.filter { $0.name.contains("Movie") || $0.name.contains("Film") } - } - } - - func performSearch() { - guard let module = module, !searchText.isEmpty else { return } - - let encodedSearchText = searchText.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? searchText - let parameter = module.module[0].search.parameter - let urlString: String - - if parameter == "blank" { - urlString = "\(module.module[0].search.url)\(encodedSearchText)" - } else { - urlString = "\(module.module[0].search.url)?\(parameter)=\(encodedSearchText)" - } - - guard let url = URL(string: urlString) else { return } - - URLSession.custom.dataTask(with: url) { data, _, error in - defer { isLoading = false } - guard let data = data, error == nil else { return } - - do { - let html = String(data: data, encoding: .utf8) ?? "" - let document = try SwiftSoup.parse(html) - let elements = try document.select(module.module[0].search.documentSelector) - - var results: [ItemResult] = [] - for element in elements { - let title = try element.select(module.module[0].search.title).text() - let href = try element.select(module.module[0].search.href).attr("href") - var imageURL = try element.select(module.module[0].search.image.url).attr(module.module[0].search.image.attribute) - - if imageURL.contains(",") { - imageURL = imageURL.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.first ?? imageURL - } - - if !imageURL.starts(with: "http") { - imageURL = "\(module.module[0].details.baseURL.hasSuffix("/") ? module.module[0].details.baseURL : "\(module.module[0].details.baseURL)/")\(imageURL.hasPrefix("/") ? String(imageURL.dropFirst()) : imageURL)" - } - - imageURL = imageURL.replacingOccurrences(of: " ", with: "%20") - - // If imageURL is not available or is the same as the baseURL, use a default image - if imageURL.isEmpty || imageURL == module.module[0].details.baseURL + "/" { - imageURL = "https://s4.anilist.co/file/anilistcdn/character/large/default.jpg" - } - - let result = ItemResult(name: title, imageUrl: imageURL, href: href) - results.append(result) - } - - // Filter out non-searchable modules - if module.module[0].search.searchable == false { - results = results.filter { $0.name.lowercased().contains(searchText.lowercased()) } - } - - DispatchQueue.main.async { - self.searchResults = results - } - } catch { - print("Error parsing HTML: \(error)") - Logger.shared.log("Error parsing HTML: \(error)") - } - }.resume() - } -} diff --git a/Sora/Views/SearchViews/SearchView.swift b/Sora/Views/SearchViews/SearchView.swift deleted file mode 100644 index 441e655..0000000 --- a/Sora/Views/SearchViews/SearchView.swift +++ /dev/null @@ -1,149 +0,0 @@ -// -// SearchView.swift -// Sora -// -// Created by Francesco on 18/12/24. -// - -import SwiftUI - -struct ItemResult: Identifiable { - let id = UUID() - let name: String - let imageUrl: String - let href: String -} - -struct SearchView: View { - @State private var searchText: String = "" - @State private var searchResults: [ItemResult] = [] - @State private var navigateToResults: Bool = false - @State private var selectedModule: ModuleStruct? - @State private var showAlert = false - @StateObject private var modulesManager = ModulesManager() - @ObservedObject private var searchHistoryManager = HistoryManager() - - var body: some View { - NavigationView { - VStack { - HStack { - Menu { - ForEach(modulesManager.modules, id: \.name) { module in - Button(action: { - selectedModule = module - }) { - Text(module.name) - } - } - } label: { - Label(selectedModule?.name ?? "Select Module", systemImage: "chevron.down") - } - - Spacer() - - SearchBar(text: $searchText, onSearchButtonClicked: { - if let _ = selectedModule, !searchText.isEmpty { - searchHistoryManager.addSearchHistory(searchText) - navigateToResults = true - } else { - showAlert = true - Logger.shared.log("No Module is selected for the search") - } - }) - } - .padding(.horizontal) - - List { - if !searchHistoryManager.searchHistory.isEmpty { - Section(header: Text("Search History")) { - ForEach(searchHistoryManager.searchHistory, id: \.self) { historyItem in - Button(action: { - searchText = historyItem - if let _ = selectedModule, !searchText.isEmpty { - navigateToResults = true - } else { - showAlert = true - Logger.shared.log("No Module is selected for the search") - } - }) { - Text(historyItem) - .foregroundColor(.primary) - } - } - .onDelete(perform: searchHistoryManager.deleteHistoryItem) - } - } - } - .navigationTitle("Search") - .onSubmit(of: .search) { - if let _ = selectedModule, !searchText.isEmpty { - navigateToResults = true - } else { - showAlert = true - Logger.shared.log("No Module is selected for the search") - } - } - - NavigationLink( - destination: SearchResultsView(module: selectedModule, searchText: searchText), - isActive: $navigateToResults, - label: { - EmptyView() - } - ) - .hidden() - } - .onAppear { - modulesManager.loadModules() - NotificationCenter.default.addObserver(forName: .moduleAdded, object: nil, queue: .main) { _ in - modulesManager.loadModules() - } - NotificationCenter.default.addObserver(forName: .moduleRemoved, object: nil, queue: .main) { _ in - modulesManager.loadModules() - } - } - .alert(isPresented: $showAlert) { - Alert( - title: Text("No module selected"), - message: Text("Please select a module before searching."), - dismissButton: .default(Text("OK")) - ) - } - } - .navigationViewStyle(StackNavigationViewStyle()) - } -} - -struct SearchBar: View { - @Binding var text: String - var onSearchButtonClicked: () -> Void - - var body: some View { - HStack { - TextField("Search...", text: $text, onCommit: onSearchButtonClicked) - .padding(7) - .padding(.horizontal, 25) - .background(Color(.systemGray6)) - .cornerRadius(8) - .overlay( - HStack { - Image(systemName: "magnifyingglass") - .foregroundColor(.gray) - .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) - .padding(.leading, 8) - - if !text.isEmpty { - Button(action: { - self.text = "" - }) { - Image(systemName: "multiply.circle.fill") - .foregroundColor(.gray) - .padding(.trailing, 8) - } - } - } - ) - .padding(.horizontal, 10) - } - } -} diff --git a/Sora/Views/SettingsSubViews/SettingsViewModule.swift b/Sora/Views/SettingsSubViews/SettingsViewModule.swift new file mode 100644 index 0000000..58f4eb3 --- /dev/null +++ b/Sora/Views/SettingsSubViews/SettingsViewModule.swift @@ -0,0 +1,132 @@ +// +// SettingsViewModule.swift +// Sora +// +// Created by Francesco on 05/01/25. +// + +import SwiftUI +import Kingfisher + +struct SettingsViewModule: View { + @AppStorage("selectedModuleId") private var selectedModuleId: String? + @EnvironmentObject var moduleManager: ModuleManager + + @State private var errorMessage: String? + @State private var isLoading = false + + var body: some View { + VStack { + Form { + ForEach(moduleManager.modules) { module in + HStack { + KFImage(URL(string: module.metadata.iconUrl)) + .resizable() + .frame(width: 50, height: 50) + .clipShape(Circle()) + .padding(.trailing, 10) + + VStack(alignment: .leading) { + HStack(alignment: .bottom, spacing: 4) { + Text(module.metadata.sourceName) + .font(.headline) + .foregroundColor(.primary) + Text("v\(module.metadata.version)") + .font(.subheadline) + .foregroundColor(.secondary) + } + Text("Author: \(module.metadata.author)") + .font(.subheadline) + .foregroundColor(.secondary) + Text("Language: \(module.metadata.language)") + .font(.subheadline) + .foregroundColor(.secondary) + } + + Spacer() + + if module.id.uuidString == selectedModuleId { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.accentColor) + .frame(width: 25, height: 25) + } + } + .contentShape(Rectangle()) + .onTapGesture { + selectedModuleId = module.id.uuidString + } + .contextMenu { + Button(action: { + UIPasteboard.general.string = module.metadata.iconUrl + }) { + Label("Copy URL", systemImage: "doc.on.doc") + } + Button(role: .destructive) { + if selectedModuleId == module.id.uuidString { + selectedModuleId = nil + } + moduleManager.deleteModule(module) + } label: { + Label("Delete", systemImage: "trash") + } + } + .swipeActions { + Button(role: .destructive) { + if selectedModuleId == module.id.uuidString { + selectedModuleId = nil + } + moduleManager.deleteModule(module) + } label: { + Label("Delete", systemImage: "trash") + } + } + } + } + .navigationTitle("Modules") + .navigationBarItems(trailing: Button(action: { + showAddModuleAlert() + }) { + Image(systemName: "plus") + .resizable() + .padding(5) + }) + } + } + + func showAddModuleAlert() { + let alert = UIAlertController(title: "Add Module", message: "Enter the URL of the module file", preferredStyle: .alert) + alert.addTextField { textField in + textField.placeholder = "https://real.url/module.json" + } + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + alert.addAction(UIAlertAction(title: "Add", style: .default, handler: { _ in + if let url = alert.textFields?.first?.text { + addModule(from: url) + } + })) + + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + rootViewController.present(alert, animated: true, completion: nil) + } + } + + private func addModule(from url: String) { + isLoading = true + errorMessage = nil + + Task { + do { + _ = try await moduleManager.addModule(metadataUrl: url) + DispatchQueue.main.async { + isLoading = false + } + } catch { + DispatchQueue.main.async { + isLoading = false + errorMessage = error.localizedDescription + } + } + } + } +} diff --git a/Sora/Views/SettingsView.swift b/Sora/Views/SettingsView.swift new file mode 100644 index 0000000..a3b059c --- /dev/null +++ b/Sora/Views/SettingsView.swift @@ -0,0 +1,139 @@ +// +// SettingsView.swift +// Sora +// +// Created by Francesco on 05/01/25. +// + +import SwiftUI + +struct SettingsView: View { + @EnvironmentObject var settings: Settings + + var body: some View { + NavigationView { + Form { + Section(header: Text("Interface")) { + ColorPicker("Accent Color", selection: $settings.accentColor) + HStack() { + Text("Appearance") + Picker("Appearance", selection: $settings.selectedAppearance) { + Text("System").tag(Appearance.system) + Text("Light").tag(Appearance.light) + Text("Dark").tag(Appearance.dark) + } + .pickerStyle(SegmentedPickerStyle()) + } + } + + Section(header: Text("External Features")) { + NavigationLink(destination: SettingsViewModule()) { + Text("Modules") + } + } + + Section(header: Text("Info")) { + Button(action: { + if let url = URL(string: "https://github.com/cranci1/Sora") { + UIApplication.shared.open(url) + } + }) { + HStack { + Text("Sora github repo") + .foregroundColor(.primary) + Spacer() + Image(systemName: "safari") + .foregroundColor(.secondary) + } + } + Button(action: { + if let url = URL(string: "https://github.com/cranci1/Sora/issues") { + UIApplication.shared.open(url) + } + }) { + HStack { + Text("Report an issue") + .foregroundColor(.primary) + Spacer() + Image(systemName: "safari") + .foregroundColor(.secondary) + } + } + Button(action: { + if let url = URL(string: "https://discord.gg/x7hppDWFDZ") { + UIApplication.shared.open(url) + } + }) { + HStack { + Text("Join the Discord") + .foregroundColor(.primary) + Spacer() + Image(systemName: "safari") + .foregroundColor(.secondary) + } + } + } + } + .navigationTitle("Settings") + } + .navigationViewStyle(StackNavigationViewStyle()) + } +} + +enum Appearance: String, CaseIterable, Identifiable { + case system, light, dark + + var id: String { self.rawValue } +} + +class Settings: ObservableObject { + @Published var accentColor: Color { + didSet { + saveAccentColor(accentColor) + } + } + @Published var selectedAppearance: Appearance { + didSet { + UserDefaults.standard.set(selectedAppearance.rawValue, forKey: "selectedAppearance") + updateAppearance() + } + } + + init() { + if let colorData = UserDefaults.standard.data(forKey: "accentColor"), + let uiColor = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(colorData) as? UIColor { + self.accentColor = Color(uiColor) + } else { + self.accentColor = .accentColor + } + if let appearanceRawValue = UserDefaults.standard.string(forKey: "selectedAppearance"), + let appearance = Appearance(rawValue: appearanceRawValue) { + self.selectedAppearance = appearance + } else { + self.selectedAppearance = .system + } + updateAppearance() + } + + private func saveAccentColor(_ color: Color) { + let uiColor = UIColor(color) + do { + let colorData = try NSKeyedArchiver.archivedData(withRootObject: uiColor, requiringSecureCoding: false) + UserDefaults.standard.set(colorData, forKey: "accentColor") + } catch { + print("Failed to save accent color: \(error.localizedDescription)") + } + } + + func updateAppearance() { + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return } + switch selectedAppearance { + case .system: + windowScene.windows.first?.overrideUserInterfaceStyle = .unspecified + case .light: + windowScene.windows.first?.overrideUserInterfaceStyle = .light + case .dark: + windowScene.windows.first?.overrideUserInterfaceStyle = .dark + } + } +} diff --git a/Sora/Views/SettingsViews/SettingView.swift b/Sora/Views/SettingsViews/SettingView.swift deleted file mode 100644 index de3c813..0000000 --- a/Sora/Views/SettingsViews/SettingView.swift +++ /dev/null @@ -1,303 +0,0 @@ -// -// SettingView.swift -// Sora -// -// Created by Francesco on 18/12/24. -// - -import SwiftUI -import Kingfisher -import UniformTypeIdentifiers - -struct SettingsView: View { - @EnvironmentObject var settings: Settings - @State private var isDocumentPickerPresented = false - @State private var showImportSuccessAlert = false - @State private var showImportFailAlert = false - @State private var importErrorMessage = "" - @State private var miruDataToImport: MiruDataStruct? - @State private var selectedModule: ModuleStruct? - @StateObject private var libraryManager = LibraryManager.shared - @StateObject private var modulesManager = ModulesManager() - - var body: some View { - NavigationView { - Form { - Section(header: Text("Interface")) { - ColorPicker("Accent Color", selection: $settings.accentColor) - HStack() { - Text("Appearance") - Picker("Appearance", selection: $settings.selectedAppearance) { - Text("System").tag(Appearance.system) - Text("Light").tag(Appearance.light) - Text("Dark").tag(Appearance.dark) - } - .pickerStyle(SegmentedPickerStyle()) - } - NavigationLink(destination: SettingsIUView()) { - Text("Interface Settings") - } - NavigationLink(destination: SettingsPlayerView()) { - Text("Media Player") - } - } - - Section(header: Text("External Features")) { - NavigationLink(destination: SettingsModuleView()) { - HStack { - Image(systemName: "puzzlepiece.fill") - Text("Modules") - } - } - NavigationLink(destination: SettingsStorageView()) { - HStack { - Image(systemName: "externaldrive.fill") - Text("Storage") - } - } - ForEach(modulesManager.modules.filter { $0.extractor == "dub-sub" }, id: \.name) { module in - Button(action: { - isDocumentPickerPresented = true - selectedModule = module - }) { - HStack { - Image(systemName: "tray.and.arrow.down.fill") - Text("Import Miru Bookmarks into \(module.name)") - } - } - } - } - - Section(header: Text("Debug")) { - NavigationLink(destination: SettingsLogsView()) { - HStack { - Image(systemName: "doc.text.fill") - Text("Logs") - } - } - NavigationLink(destination: SettingsEditorView(modulesManager: ModulesManager())) { - HStack { - Image(systemName: "pencil.and.outline") - Text("Modules Editor") - } - } - } - - Section(header: Text("Info")) { - NavigationLink(destination: AboutView()) { - Text("About") - } - NavigationLink(destination: SettingsReleasesView()) { - Text("Releases") - } - Button(action: { - if let url = URL(string: "https://github.com/cranci1/Sora") { - UIApplication.shared.open(url) - } - }) { - HStack { - Text("Sora github repo") - .foregroundColor(.primary) - Spacer() - Image(systemName: "safari") - .foregroundColor(.secondary) - } - } - Button(action: { - if let url = URL(string: "https://github.com/cranci1/Sora/issues") { - UIApplication.shared.open(url) - } - }) { - HStack { - Text("Report an issue") - .foregroundColor(.primary) - Spacer() - Image(systemName: "safari") - .foregroundColor(.secondary) - } - } - Button(action: { - if let url = URL(string: "https://discord.gg/x7hppDWFDZ") { - UIApplication.shared.open(url) - } - }) { - HStack { - Text("Join the Discord") - .foregroundColor(.primary) - Spacer() - Image(systemName: "safari") - .foregroundColor(.secondary) - } - } - } - } - .navigationTitle("Settings") - .sheet(isPresented: $isDocumentPickerPresented) { - DocumentPicker( - libraryManager: libraryManager, - onSuccess: { miruData in - miruDataToImport = miruData - if let selectedModule = selectedModule { - libraryManager.importFromMiruData(miruData, module: selectedModule) - showImportSuccessAlert = true - } - }, - onFailure: { errorMessage in - importErrorMessage = errorMessage - showImportFailAlert = true - } - ) - } - .alert("Data Imported!", isPresented: $showImportSuccessAlert) { - Button("OK", role: .cancel) { } - } message: { - Text("Miru bookmarks are now imported in Sora, enjoy!") - } - .alert("Import Failed", isPresented: $showImportFailAlert) { - Button("OK", role: .cancel) { } - } message: { - Text(importErrorMessage) - } - } - .navigationViewStyle(StackNavigationViewStyle()) - } -} - -struct DocumentPicker: UIViewControllerRepresentable { - var libraryManager: LibraryManager - var onSuccess: (MiruDataStruct) -> Void - var onFailure: (String) -> Void - - func makeCoordinator() -> Coordinator { - Coordinator(self, libraryManager: libraryManager, onSuccess: onSuccess, onFailure: onFailure) - } - - func makeUIViewController(context: Context) -> UIDocumentPickerViewController { - let picker = UIDocumentPickerViewController(forOpeningContentTypes: [UTType.json]) - picker.delegate = context.coordinator - return picker - } - - func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {} - - class Coordinator: NSObject, UIDocumentPickerDelegate { - var parent: DocumentPicker - var libraryManager: LibraryManager - var onSuccess: (MiruDataStruct) -> Void - var onFailure: (String) -> Void - - init(_ parent: DocumentPicker, libraryManager: LibraryManager, onSuccess: @escaping (MiruDataStruct) -> Void, onFailure: @escaping (String) -> Void) { - self.parent = parent - self.libraryManager = libraryManager - self.onSuccess = onSuccess - self.onFailure = onFailure - } - - func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { - guard let selectedFileURL = urls.first else { - let errorMessage = "No file URL selected" - print(errorMessage) - Logger.shared.log(errorMessage) - onFailure(errorMessage) - return - } - - guard selectedFileURL.startAccessingSecurityScopedResource() else { - let errorMessage = "Could not access the file" - print(errorMessage) - Logger.shared.log("Could not access the Miru Backup File") - onFailure(errorMessage) - return - } - - defer { - selectedFileURL.stopAccessingSecurityScopedResource() - } - - do { - let data = try Data(contentsOf: selectedFileURL) - var miruData = try JSONDecoder().decode(MiruDataStruct.self, from: data) - - miruData.likes = miruData.likes.map { like in - var updatedLike = like - updatedLike.gogoSlug = "/series/" + like.gogoSlug - return updatedLike - } - - Logger.shared.log("Imported Miru data from \(selectedFileURL)") - onSuccess(miruData) - } catch { - let errorMessage = "Failed to import Miru data: \(error.localizedDescription)" - print(errorMessage) - Logger.shared.log(errorMessage) - onFailure(errorMessage) - } - } - - func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { - let errorMessage = "Document picker was closed" - print(errorMessage) - Logger.shared.log(errorMessage) - onFailure(errorMessage) - } - } -} - -enum Appearance: String, CaseIterable, Identifiable { - case system, light, dark - - var id: String { self.rawValue } -} - -class Settings: ObservableObject { - @Published var accentColor: Color { - didSet { - saveAccentColor(accentColor) - } - } - @Published var selectedAppearance: Appearance { - didSet { - UserDefaults.standard.set(selectedAppearance.rawValue, forKey: "selectedAppearance") - updateAppearance() - } - } - - init() { - if let colorData = UserDefaults.standard.data(forKey: "accentColor"), - let uiColor = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(colorData) as? UIColor { - self.accentColor = Color(uiColor) - } else { - self.accentColor = .accentColor - } - if let appearanceRawValue = UserDefaults.standard.string(forKey: "selectedAppearance"), - let appearance = Appearance(rawValue: appearanceRawValue) { - self.selectedAppearance = appearance - } else { - self.selectedAppearance = .system - } - updateAppearance() - } - - private func saveAccentColor(_ color: Color) { - let uiColor = UIColor(color) - do { - let colorData = try NSKeyedArchiver.archivedData(withRootObject: uiColor, requiringSecureCoding: false) - UserDefaults.standard.set(colorData, forKey: "accentColor") - } catch { - print("Failed to save accent color: \(error.localizedDescription)") - Logger.shared.log("Failed to save accent color: \(error.localizedDescription)") - } - } - - func updateAppearance() { - guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return } - switch selectedAppearance { - case .system: - windowScene.windows.first?.overrideUserInterfaceStyle = .unspecified - case .light: - windowScene.windows.first?.overrideUserInterfaceStyle = .light - case .dark: - windowScene.windows.first?.overrideUserInterfaceStyle = .dark - } - } -} diff --git a/Sora/Views/SettingsViews/SubPages/SettingsAboutView.swift b/Sora/Views/SettingsViews/SubPages/SettingsAboutView.swift deleted file mode 100644 index c9f1b02..0000000 --- a/Sora/Views/SettingsViews/SubPages/SettingsAboutView.swift +++ /dev/null @@ -1,293 +0,0 @@ -// -// SettingsAboutView.swift -// Sora -// -// Created by Francesco on 18/12/24. -// - -import SwiftUI -import Kingfisher - -struct AboutView: View { - var body: some View { - Form { - Section(footer: Text("Sora is a free open source app, under the GPLv3.0 License. You can find the entire Sora code in the github repo.")) { - HStack(alignment: .center, spacing: 10) { - KFImage(URL(string: "https://raw.githubusercontent.com/cranci1/Sora/main/Sora/Assets.xcassets/AppIcon.appiconset/180.png")) - .resizable() - .frame(width: 80, height: 80) - .clipShape(Circle()) - - VStack(alignment: .leading) { - Text("Sora") - .font(.largeTitle) - .fontWeight(.bold) - - Text("Public beta 0.1.1") - .font(.subheadline) - .foregroundColor(.secondary) - } - } - .padding(.vertical) - } - - Section(header: Text("Developer")) { - Button(action: { - if let url = URL(string: "https://github.com/cranci1") { - UIApplication.shared.open(url) - } - }) { - HStack { - KFImage(URL(string: "https://avatars.githubusercontent.com/u/100066266?v=4")) - .resizable() - .frame(width: 40, height: 40) - .clipShape(Circle()) - - VStack(alignment: .leading) { - Text("cranci1") - .font(.headline) - .foregroundColor(.pink) - Text("YAY it's me") - .font(.subheadline) - .foregroundColor(.secondary) - } - Spacer() - Image(systemName: "safari") - .foregroundColor(.pink) - } - } - } - - Section(header: Text("Huge thanks"), footer: Text("A huge thanks to the Miru Development team for their support and contributions to Sora. I wont ever be able to thank them enough. Thanks a lot to them and all my discord helper.")) { - HStack { - KFImage(URL(string: "https://storage.ko-fi.com/cdn/useruploads/e68c31f0-7e66-4d63-934a-0508ce443bc0_e71506-30ce-4a01-9ac3-892ffcd18b77.png")) - .resizable() - .frame(width: 40, height: 40) - .clipShape(Circle()) - - Text("Miru Development Team") - .font(.headline) - .foregroundColor(.green) - } - Button(action: { - if let url = URL(string: "https://github.com/bshar1865") { - UIApplication.shared.open(url) - } - }) { - HStack { - KFImage(URL(string: "https://avatars.githubusercontent.com/u/98615778?v=4")) - .resizable() - .frame(width: 40, height: 40) - .clipShape(Circle()) - - VStack(alignment: .leading) { - Text("MA.") - .font(.headline) - .foregroundColor(.orange) - Text("Discord Helper") - .font(.subheadline) - .foregroundColor(.secondary) - } - Spacer() - Image(systemName: "safari") - .foregroundColor(.orange) - } - } - Button(action: { - if let url = URL(string: "https://github.com/50n50") { - UIApplication.shared.open(url) - } - }) { - HStack { - KFImage(URL(string: "https://avatars.githubusercontent.com/u/80717571?v=4")) - .resizable() - .frame(width: 40, height: 40) - .clipShape(Circle()) - - VStack(alignment: .leading) { - Text("50/50") - .font(.headline) - .foregroundColor(.mint) - Text("Discord Helper & Designer") - .font(.subheadline) - .foregroundColor(.secondary) - } - Spacer() - Image(systemName: "safari") - .foregroundColor(.mint) - } - } - Button(action: { - if let url = URL(string: "https://github.com/IBH-RAD") { - UIApplication.shared.open(url) - } - }) { - HStack { - KFImage(URL(string: "https://avatars.githubusercontent.com/u/116025932?u=393be7ee3f476362b9e09d4f195ac035c5060236&v=4")) - .resizable() - .frame(width: 40, height: 40) - .clipShape(Circle()) - - VStack(alignment: .leading) { - Text("IBH") - .font(.headline) - .foregroundColor(.purple) - Text("Discord Helper & Bug Hunter") - .font(.subheadline) - .foregroundColor(.secondary) - } - Spacer() - Image(systemName: "safari") - .foregroundColor(.purple) - } - } - Button(action: { - if let url = URL(string: "https://github.com/Seeike") { - UIApplication.shared.open(url) - } - }) { - HStack { - KFImage(URL(string: "https://avatars.githubusercontent.com/u/122684677?v=4")) - .resizable() - .frame(width: 40, height: 40) - .clipShape(Circle()) - - VStack(alignment: .leading) { - Text("Seiike") - .font(.headline) - .foregroundColor(.yellow) - Text("Discord Helper") - .font(.subheadline) - .foregroundColor(.secondary) - } - Spacer() - Image(systemName: "safari") - .foregroundColor(.yellow) - } - } - } - - Section(header: Text("Acknowledgements"), footer: Text("Thanks to the creators of this frameworks, that made Sora creation much simplier.")) { - Button(action: { - if let url = URL(string: "https://github.com/scinfu/SwiftSoup") { - UIApplication.shared.open(url) - } - }) { - HStack { - KFImage(URL(string: "https://raw.githubusercontent.com/scinfu/SwiftSoup/master/swiftsoup.png")) - .resizable() - .frame(width: 40, height: 40) - - VStack(alignment: .leading) { - Text("SwiftSoup") - .font(.headline) - .foregroundColor(.red) - Text("Web scraping") - .font(.subheadline) - .foregroundColor(.secondary) - } - Spacer() - Image(systemName: "safari") - .foregroundColor(.red) - } - } - Button(action: { - if let url = URL(string: "https://github.com/onevcat/Kingfisher") { - UIApplication.shared.open(url) - } - }) { - HStack { - KFImage(URL(string: "https://products.fileformat.com/image/swift/kingfisher/header-image.png")) - .resizable() - .frame(width: 40, height: 40) - - VStack(alignment: .leading) { - Text("Kingfisher") - .font(.headline) - .foregroundColor(.blue) - Text("Images caching and loading") - .font(.subheadline) - .foregroundColor(.secondary) - } - Spacer() - Image(systemName: "safari") - .foregroundColor(.blue) - } - } - Button(action: { - if let url = URL(string: "https://github.com/ipavlidakis/OpenCastSwift") { - UIApplication.shared.open(url) - } - }) { - HStack { - KFImage(URL(string: "https://avatars.githubusercontent.com/u/575802?v=4")) - .resizable() - .frame(width: 40, height: 40) - .clipShape(Circle()) - - VStack(alignment: .leading) { - Text("OpenCastSwift") - .font(.headline) - .foregroundColor(.green) - Text("Casting support") - .font(.subheadline) - .foregroundColor(.secondary) - } - Spacer() - Image(systemName: "safari") - .foregroundColor(.green) - } - } - Button(action: { - if let url = URL(string: "https://github.com/SwiftyJSON/SwiftyJSON") { - UIApplication.shared.open(url) - } - }) { - HStack { - KFImage(URL(string: "https://avatars.githubusercontent.com/u/8858017?s=200&v=4")) - .resizable() - .frame(width: 40, height: 40) - .clipShape(Circle()) - - VStack(alignment: .leading) { - Text("SwiftyJSON") - .font(.headline) - .foregroundColor(.orange) - Text("Opencast dependency") - .font(.subheadline) - .foregroundColor(.secondary) - } - Spacer() - Image(systemName: "safari") - .foregroundColor(.orange) - } - } - Button(action: { - if let url = URL(string: "https://github.com/apple/swift-protobuf") { - UIApplication.shared.open(url) - } - }) { - HStack { - KFImage(URL(string: "https://avatars.githubusercontent.com/u/10639145?s=200&v=4")) - .resizable() - .frame(width: 40, height: 40) - .clipShape(Circle()) - - VStack(alignment: .leading) { - Text("Swift Protobuf") - .font(.headline) - .foregroundColor(.purple) - Text("Opencast dependency") - .font(.subheadline) - .foregroundColor(.secondary) - } - Spacer() - Image(systemName: "safari") - .foregroundColor(.purple) - } - } - } - } - .navigationTitle("About") - } -} diff --git a/Sora/Views/SettingsViews/SubPages/SettingsEditorView.swift b/Sora/Views/SettingsViews/SubPages/SettingsEditorView.swift deleted file mode 100644 index c41a9a2..0000000 --- a/Sora/Views/SettingsViews/SubPages/SettingsEditorView.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// SettingsEditorView.swift -// Sora -// -// Created by Francesco on 03/01/25. -// - -import SwiftUI - -struct SettingsEditorView: View { - @ObservedObject var modulesManager: ModulesManager - @State private var jsonText: String = "" - - var body: some View { - VStack { - TextEditor(text: $jsonText) - .padding() - .onAppear { - if let data = try? JSONEncoder().encode(modulesManager.modules), - let jsonString = String(data: data, encoding: .utf8) { - jsonText = jsonString - } - } - } - .navigationTitle("Editor") - .navigationBarItems(trailing: Button("Save") { - if let data = jsonText.data(using: .utf8), - let modules = try? JSONDecoder().decode([ModuleStruct].self, from: data) { - modulesManager.modules = modules - modulesManager.saveModuleData() - } - }) - } -} \ No newline at end of file diff --git a/Sora/Views/SettingsViews/SubPages/SettingsIUView.swift b/Sora/Views/SettingsViews/SubPages/SettingsIUView.swift deleted file mode 100644 index 24953b8..0000000 --- a/Sora/Views/SettingsViews/SubPages/SettingsIUView.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// SettingsIUView.swift -// Sora -// -// Created by Francesco on 18/12/24. -// - -import SwiftUI - -struct SettingsIUView: View { - @AppStorage("listSearch") private var isListSearchEnabled: Bool = false - - var body: some View { - Form { - Section(header: Text("Search")) { - Toggle("List Search Style", isOn: $isListSearchEnabled) - .tint(.accentColor) - } - } - .navigationTitle("Interface Preference") - } -} diff --git a/Sora/Views/SettingsViews/SubPages/SettingsLogsView.swift b/Sora/Views/SettingsViews/SubPages/SettingsLogsView.swift deleted file mode 100644 index 07bbeae..0000000 --- a/Sora/Views/SettingsViews/SubPages/SettingsLogsView.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// SettingsLogsView.swift -// Sora -// -// Created by Francesco on 18/12/24. -// - -import SwiftUI - -struct SettingsLogsView: View { - @State private var logs: String = "" - - var body: some View { - VStack { - ScrollView { - Text(logs) - .font(.footnote) - .foregroundColor(.secondary) - .frame(maxWidth: .infinity, alignment: .leading) - .padding() - .textSelection(.enabled) - } - .navigationTitle("Logs") - .onAppear { - logs = Logger.shared.getLogs() - } - } - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Menu { - Button(action: { - UIPasteboard.general.string = logs - }) { - Label("Copy to Clipboard", systemImage: "doc.on.doc") - } - Button(role: .destructive, action: { - Logger.shared.clearLogs() - logs = Logger.shared.getLogs() - }) { - Label("Clear Logs", systemImage: "trash") - } - } label: { - Image(systemName: "ellipsis.circle") - .resizable() - .frame(width: 20, height: 20) - } - } - } - } -} - -class Logger { - static let shared = Logger() - private var logs: [(message: String, timestamp: Date)] = [] - - private init() {} - - func log(_ message: String) { - logs.append((message: message, timestamp: Date())) - } - - func getLogs() -> String { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" - return logs.map { "[\(dateFormatter.string(from: $0.timestamp))] \($0.message)" } - .joined(separator: "\n---\n") - } - - func clearLogs() { - logs.removeAll() - } -} diff --git a/Sora/Views/SettingsViews/SubPages/SettingsModuleView.swift b/Sora/Views/SettingsViews/SubPages/SettingsModuleView.swift deleted file mode 100644 index 77faa47..0000000 --- a/Sora/Views/SettingsViews/SubPages/SettingsModuleView.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// SettingsModuleView.swift -// Sora -// -// Created by Francesco on 18/12/24. -// - -import SwiftUI -import Kingfisher - -struct ErrorMessage: Identifiable { - var id: String { message } - let message: String -} - -struct SettingsModuleView: View { - @StateObject private var modulesManager = ModulesManager() - @State private var showingAddModuleAlert = false - @State private var moduleURL = "" - @State private var errorMessage: ErrorMessage? - @State private var previusImageURLs: [String: String] = [:] - - var body: some View { - VStack { - if modulesManager.isLoading { - ProgressView("Loading Modules...") - } else { - List { - ForEach(modulesManager.modules, id: \.name) { module in - HStack { - if let url = URL(string: module.iconURL) { - if previusImageURLs[module.name] != module.iconURL { - KFImage(url) - .resizable() - .frame(width: 50, height: 50) - .clipShape(Circle()) - .padding(.trailing, 10) - .onAppear { - previusImageURLs[module.name] = module.iconURL - } - } else { - KFImage(url) - .resizable() - .frame(width: 50, height: 50) - .clipShape(Circle()) - .padding(.trailing, 10) - } - } - VStack(alignment: .leading) { - Text(module.name) - .font(.headline) - .foregroundColor(.primary) - Text("Version: \(module.version)") - .font(.subheadline) - .foregroundColor(.secondary) - Text("Author: \(module.author.name)") - .font(.subheadline) - .foregroundColor(.secondary) - Text("Language: \(module.language)") - .font(.subheadline) - .foregroundColor(.secondary) - } - Spacer() - Text(module.stream) - .font(.caption) - .padding(5) - .background(Color.accentColor) - .foregroundColor(Color.primary) - .clipShape(Capsule()) - } - .contextMenu { - Button(action: { - UIPasteboard.general.string = modulesManager.moduleURLs[module.name] - }) { - Label("Copy URL", systemImage: "doc.on.doc") - } - Button(role: .destructive, action: { - modulesManager.deleteModule(named: module.name) - }) { - Label("Delete", systemImage: "trash") - } - } - } - .onDelete(perform: deleteModule) - } - .navigationBarTitle("Modules") - .navigationBarItems(trailing: Button(action: { - showAddModuleAlert() - }) { - Image(systemName: "plus") - .resizable() - .frame(width: 20, height: 20) - }) - .refreshable { - modulesManager.refreshModules() - } - } - } - .onAppear { - modulesManager.loadModules() - } - .alert(item: $errorMessage) { error in - Alert(title: Text("Error"), message: Text(error.message), dismissButton: .default(Text("OK"))) - } - } - - func showAddModuleAlert() { - let alert = UIAlertController(title: "Add Module", message: "Enter the URL of the module file", preferredStyle: .alert) - alert.addTextField { textField in - textField.placeholder = "https://real.url/module.json" - } - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) - alert.addAction(UIAlertAction(title: "Add", style: .default, handler: { _ in - if let url = alert.textFields?.first?.text { - modulesManager.addModule(from: url) { result in - switch result { - case .success: - break - case .failure(let error): - errorMessage = ErrorMessage(message: error.localizedDescription) - Logger.shared.log(error.localizedDescription.capitalized) - } - } - } - })) - - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootViewController = windowScene.windows.first?.rootViewController { - rootViewController.present(alert, animated: true, completion: nil) - } - } - - func deleteModule(at offsets: IndexSet) { - offsets.forEach { index in - let module = modulesManager.modules[index] - modulesManager.deleteModule(named: module.name) - } - } -} diff --git a/Sora/Views/SettingsViews/SubPages/SettingsPlayerView.swift b/Sora/Views/SettingsViews/SubPages/SettingsPlayerView.swift deleted file mode 100644 index 195997a..0000000 --- a/Sora/Views/SettingsViews/SubPages/SettingsPlayerView.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// SettingsPlayerView.swift -// Sora -// -// Created by Francesco on 18/12/24. -// - -import SwiftUI - -struct SettingsPlayerView: View { - @AppStorage("externalPlayer") private var externalPlayer: String = "Default" - @AppStorage("AlwaysLandscape") private var isAlwaysLandscape = false - @AppStorage("hideNextButton") private var isHideNextButton = false - @AppStorage("holdSpeedPlayer") private var holdSpeedPlayer: Double = 2.0 - - var body: some View { - Form { - Section(header: Text("Media Player"), footer: Text("The Force Landscape and HoldSpeed only work inside the default iOS player and Sora player.")) { - HStack { - Text("Media Player") - Spacer() - Menu(externalPlayer) { - Button(action: { - externalPlayer = "Default" - }) { - Label("Default", systemImage: externalPlayer == "Default" ? "checkmark" : "") - } - Button(action: { - externalPlayer = "VLC" - }) { - Label("VLC", systemImage: externalPlayer == "VLC" ? "checkmark" : "") - } - Button(action: { - externalPlayer = "OutPlayer" - }) { - Label("OutPlayer", systemImage: externalPlayer == "OutPlayer" ? "checkmark" : "") - } - Button(action: { - externalPlayer = "Infuse" - }) { - Label("Infuse", systemImage: externalPlayer == "Infuse" ? "checkmark" : "") - } - Button(action: { - externalPlayer = "nPlayer" - }) { - Label("nPlayer", systemImage: externalPlayer == "nPlayer" ? "checkmark" : "") - } - Button(action: { - externalPlayer = "Sora" - }) { - Label("Sora", systemImage: externalPlayer == "Sora" ? "checkmark" : "") - } - } - } - - Toggle("Hide 'Watch Next' after 5s", isOn: $isHideNextButton) - .tint(.accentColor) - - HStack { - Text("Hold Speed:") - Spacer() - Stepper( - value: $holdSpeedPlayer, - in: 0.25...2.0, - step: 0.25 - ) { - Text(String(format: "%.2f", holdSpeedPlayer)) - } - } - } - } - .navigationTitle("Player") - } -} - diff --git a/Sora/Views/SettingsViews/SubPages/SettingsReleasesView.swift b/Sora/Views/SettingsViews/SubPages/SettingsReleasesView.swift deleted file mode 100644 index bcc6c79..0000000 --- a/Sora/Views/SettingsViews/SubPages/SettingsReleasesView.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// SettingsReleasesView.swift -// Sora -// -// Created by Francesco on 31/12/24. -// - -import SwiftUI - -struct SettingsReleasesView: View { - @State private var releases: [GitHubReleases] = [] - - var body: some View { - List(releases, id: \.tagName) { release in - VStack(alignment: .leading) { - Text(release.tagName) - .font(.system(size: 17)) - .bold() - Text(release.body) - .font(.system(size: 14)) - } - .contextMenu { - Button(action: { - if let url = URL(string: release.htmlUrl) { - UIApplication.shared.open(url) - } - }) { - Text("View on GitHub") - Image(systemName: "safari") - } - } - } - .navigationTitle("Releases") - .onAppear { - GitHubAPI.shared.fetchReleases { fetchedReleases in - if let fetchedReleases = fetchedReleases { - self.releases = fetchedReleases - } - } - } - } -} diff --git a/Sora/Views/SettingsViews/SubPages/SettingsStorageView.swift b/Sora/Views/SettingsViews/SubPages/SettingsStorageView.swift deleted file mode 100644 index 991218d..0000000 --- a/Sora/Views/SettingsViews/SubPages/SettingsStorageView.swift +++ /dev/null @@ -1,269 +0,0 @@ -// -// SettingsStorageView.swift -// Sora -// -// Created by Francesco on 19/12/24. -// - -import SwiftUI - -struct SettingsStorageView: View { - @State private var appSize: String = "Calculating..." - @State private var storageDetails: [(String, Double, Color)] = [] - @State private var deviceStorage: (total: Int64, used: Int64) = (0, 0) - @State private var showingClearCacheAlert = false - @State private var showingClearDocumentsAlert = false - - var body: some View { - List { - Section { - VStack(alignment: .leading, spacing: 8) { - HStack(alignment: .center) { - Text(appSize) - .font(.system(size: 28, weight: .bold)) - Text("of \(ByteCountFormatter.string(fromByteCount: deviceStorage.total, countStyle: .file))") - .foregroundColor(.secondary) - } - - VStack(alignment: .leading, spacing: 4) { - GeometryReader { geometry in - ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 8) - .fill(Color.gray.opacity(0.2)) - .frame(height: 24) - - RoundedRectangle(cornerRadius: 8) - .fill(Color.gray.opacity(0.3)) - .frame(width: geometry.size.width * CGFloat(deviceStorage.used) / CGFloat(deviceStorage.total)) - .frame(height: 24) - - HStack(spacing: 0) { - ForEach(storageDetails, id: \.0) { detail in - RoundedRectangle(cornerRadius: 0) - .fill(detail.2) - .frame(width: geometry.size.width * CGFloat(detail.1 * 1024 * 1024) / CGFloat(deviceStorage.total)) - .frame(height: 24) - } - } - .clipShape(RoundedRectangle(cornerRadius: 8)) - } - } - .frame(height: 24) - - HStack(spacing: 16) { - ForEach(storageDetails, id: \.0) { detail in - HStack(spacing: 4) { - Circle() - .fill(detail.2) - .frame(width: 8, height: 8) - Text(detail.0) - .font(.caption) - .foregroundColor(.secondary) - } - } - HStack(spacing: 4) { - Circle() - .fill(Color.gray.opacity(0.3)) - .frame(width: 8, height: 8) - Text("System") - .font(.caption) - .foregroundColor(.secondary) - } - } - } - } - .padding(.vertical, 8) - } - - Section { - ForEach(storageDetails, id: \.0) { detail in - HStack { - Image(systemName: categoryIcon(for: detail.0)) - .foregroundColor(.white) - .frame(width: 28, height: 28) - .background(detail.2) - .clipShape(RoundedRectangle(cornerRadius: 6)) - - VStack(alignment: .leading, spacing: 2) { - Text(detail.0) - Text("\(detail.1, specifier: "%.2f") MB") - .font(.caption) - .foregroundColor(.secondary) - } - } - } - } - - Section(header: Text("Actions")) { - Button(action: { showingClearCacheAlert = true }) { - actionRow( - icon: "clock.fill", - title: "Clear Cache", - subtitle: "Free up space used by cached items", - iconColor: .accentColor - ) - } - - Button(action: { showingClearDocumentsAlert = true }) { - actionRow( - icon: "doc.fill", - title: "Clear Documents", - subtitle: "Check and remove unnecessary files", - iconColor: .accentColor - ) - } - } - } - .navigationTitle("Storage") - .onAppear { - calculateAppSize() - getDeviceStorage() - } - .alert("Clear Cache", isPresented: $showingClearCacheAlert) { - Button("Cancel", role: .cancel) { } - Button("Clear", role: .destructive) { - clearCache() - } - } message: { - Text("Are you sure you want to clear the cache? This action cannot be undone.") - } - .alert("Clear Documents", isPresented: $showingClearDocumentsAlert) { - Button("Cancel", role: .cancel) { } - Button("Clear", role: .destructive) { - clearDocuments() - } - } message: { - Text("Are you sure you want to clear all documents? This action cannot be undone.") - } - } - - private func clearCache() { - if let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first { - do { - let fileURLs = try FileManager.default.contentsOfDirectory( - at: cacheURL, - includingPropertiesForKeys: nil, - options: .skipsHiddenFiles - ) - - for fileURL in fileURLs { - try FileManager.default.removeItem(at: fileURL) - } - - calculateAppSize() - getDeviceStorage() - Logger.shared.log("Cleared Cache") - } catch { - print("Error clearing cache: \(error)") - Logger.shared.log("Error clearing cache: \(error)") - } - } - } - - private func clearDocuments() { - if let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { - do { - let fileURLs = try FileManager.default.contentsOfDirectory( - at: documentsURL, - includingPropertiesForKeys: nil, - options: .skipsHiddenFiles - ) - - for fileURL in fileURLs { - try FileManager.default.removeItem(at: fileURL) - } - - calculateAppSize() - getDeviceStorage() - Logger.shared.log("Cleared Documents") - } catch { - print("Error clearing documents: \(error)") - Logger.shared.log("Error clearing documents: \(error)") - } - } - } - - private func getDeviceStorage() { - do { - let fileURL = URL(fileURLWithPath: NSHomeDirectory() as String) - let values = try fileURL.resourceValues(forKeys: [.volumeTotalCapacityKey, .volumeAvailableCapacityKey]) - - if let total = values.volumeTotalCapacity, - let available = values.volumeAvailableCapacity { - deviceStorage.total = Int64(total) - deviceStorage.used = Int64(total - available) - } - } catch { - print("Error getting device storage: \(error)") - Logger.shared.log("Error getting device storage: \(error)") - } - } - - private func getTotalAppBytes() -> Int64 { - return Int64(totalSize() * 1024 * 1024) - } - - private func actionRow(icon: String, title: String, subtitle: String, iconColor: Color) -> some View { - HStack { - Image(systemName: icon) - .foregroundColor(.white) - .frame(width: 28, height: 28) - .background(iconColor) - .clipShape(RoundedRectangle(cornerRadius: 6)) - - VStack(alignment: .leading, spacing: 2) { - Text(title) - Text(subtitle) - .font(.caption) - .foregroundColor(.secondary) - } - } - } - - private func categoryIcon(for category: String) -> String { - switch category { - case "Documents": - return "doc.fill" - case "Cache": - return "clock.fill" - case "Temporary": - return "trash.fill" - default: - return "questionmark" - } - } - - private func calculateAppSize() { - let cacheSize = getDirectorySize(url: FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!) - let documentsSize = getDirectorySize(url: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!) - let tmpSize = getDirectorySize(url: FileManager.default.temporaryDirectory) - - let totalSize = cacheSize + documentsSize + tmpSize - self.appSize = ByteCountFormatter.string(fromByteCount: Int64(totalSize), countStyle: .file) - self.storageDetails = [ - ("Documents", documentsSize / 1024 / 1024, .green), - ("Cache", cacheSize / 1024 / 1024, .orange), - ("Temporary", tmpSize / 1024 / 1024, .red) - ] - } - - private func getDirectorySize(url: URL) -> Double { - var size: Double = 0 - if let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.fileSizeKey], options: [], errorHandler: nil) { - for case let fileURL as URL in enumerator { - do { - let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey]) - size += Double(resourceValues.fileSize ?? 0) - } catch { - print("Error calculating size for file \(fileURL): \(error)") - Logger.shared.log("Error calculating size for file \(fileURL): \(error)") - } - } - } - return size - } - - private func totalSize() -> Double { - return storageDetails.reduce(0) { $0 + $1.1 } - } -} diff --git a/macbuild.sh b/macbuild.sh deleted file mode 100755 index 1472471..0000000 --- a/macbuild.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -set -e - -cd "$(dirname "$0")" - -WORKING_LOCATION="$(pwd)" -APPLICATION_NAME=Sora - -if [ ! -d "build" ]; then - mkdir build -fi - -cd build - -xcodebuild -project "$WORKING_LOCATION/$APPLICATION_NAME.xcodeproj" \ - -scheme "$APPLICATION_NAME" \ - -configuration Release \ - -derivedDataPath "$WORKING_LOCATION/build/DerivedDataApp" \ - -destination 'platform=macOS,variant=Mac Catalyst' \ - clean build \ - CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGN_ENTITLEMENTS="" CODE_SIGNING_ALLOWED="NO" - -DD_APP_PATH="$WORKING_LOCATION/build/DerivedDataApp/Build/Products/Release-maccatalyst/$APPLICATION_NAME.app" -TARGET_APP="$WORKING_LOCATION/build/$APPLICATION_NAME.app" - -if [ -e "$TARGET_APP" ]; then - rm -rf "$TARGET_APP" -fi - -cp -r "$DD_APP_PATH" "$TARGET_APP" - -codesign --remove "$TARGET_APP" -if [ -e "$TARGET_APP/_CodeSignature" ]; then - rm -rf "$TARGET_APP/_CodeSignature" -fi - -zip -vr "$APPLICATION_NAME-catalyst.zip" "$APPLICATION_NAME.app" -rm -rf "$APPLICATION_NAME.app"