diff --git a/Sora.xcodeproj/project.pbxproj b/Sora.xcodeproj/project.pbxproj new file mode 100644 index 0000000..9823a09 --- /dev/null +++ b/Sora.xcodeproj/project.pbxproj @@ -0,0 +1,570 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 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 */; }; + 132417A42D1319E800B4F2D2 /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1324179D2D1319E800B4F2D2 /* PlayerView.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 /* AnimeInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417B62D131A0600B4F2D2 /* AnimeInfoView.swift */; }; + 132417C42D131A0600B4F2D2 /* AnimeInfoExtraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417B72D131A0600B4F2D2 /* AnimeInfoExtraction.swift */; }; + 132417C92D131B0D00B4F2D2 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 132417C82D131B0D00B4F2D2 /* Kingfisher */; }; + 132417CF2D131B7400B4F2D2 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 132417CE2D131B7400B4F2D2 /* SwiftSoup */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 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 = ""; }; + 1324179D2D1319E800B4F2D2 /* PlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerView.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 /* AnimeInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimeInfoView.swift; sourceTree = ""; }; + 132417B72D131A0600B4F2D2 /* AnimeInfoExtraction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimeInfoExtraction.swift; sourceTree = ""; }; + 132417C52D131AA500B4F2D2 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 1324177D2D13198000B4F2D2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 132417C92D131B0D00B4F2D2 /* Kingfisher in Frameworks */, + 132417CF2D131B7400B4F2D2 /* SwiftSoup in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 132417772D13198000B4F2D2 = { + isa = PBXGroup; + children = ( + 132417822D13198000B4F2D2 /* Sora */, + 132417812D13198000B4F2D2 /* Products */, + ); + sourceTree = ""; + }; + 132417812D13198000B4F2D2 /* Products */ = { + isa = PBXGroup; + children = ( + 132417802D13198000B4F2D2 /* Sora.app */, + ); + name = Products; + sourceTree = ""; + }; + 132417822D13198000B4F2D2 /* Sora */ = { + isa = PBXGroup; + children = ( + 132417C52D131AA500B4F2D2 /* Info.plist */, + 132417A52D131A0600B4F2D2 /* Views */, + 132417912D1319E800B4F2D2 /* Utils */, + 132417832D13198000B4F2D2 /* SoraApp.swift */, + 132417852D13198000B4F2D2 /* ContentView.swift */, + 132417872D13198200B4F2D2 /* Assets.xcassets */, + 132417892D13198200B4F2D2 /* Preview Content */, + ); + path = Sora; + sourceTree = ""; + }; + 132417892D13198200B4F2D2 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 1324178A2D13198200B4F2D2 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 132417912D1319E800B4F2D2 /* Utils */ = { + isa = PBXGroup; + children = ( + 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 */, + ); + 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 = ( + 1324179C2D1319E800B4F2D2 /* NormalPlayer.swift */, + 1324179D2D1319E800B4F2D2 /* PlayerView.swift */, + ); + path = Player; + sourceTree = ""; + }; + 132417A52D131A0600B4F2D2 /* Views */ = { + isa = PBXGroup; + children = ( + 132417A62D131A0600B4F2D2 /* SearchViews */, + 132417A92D131A0600B4F2D2 /* SettingsViews */, + 132417B12D131A0600B4F2D2 /* HomeView.swift */, + 132417B22D131A0600B4F2D2 /* LibraryViews */, + 132417B52D131A0600B4F2D2 /* AnimeViews */, + ); + path = Views; + sourceTree = ""; + }; + 132417A62D131A0600B4F2D2 /* SearchViews */ = { + isa = PBXGroup; + children = ( + 132417A72D131A0600B4F2D2 /* SearchView.swift */, + 132417A82D131A0600B4F2D2 /* SearchResultsView.swift */, + ); + path = SearchViews; + sourceTree = ""; + }; + 132417A92D131A0600B4F2D2 /* SettingsViews */ = { + isa = PBXGroup; + children = ( + 132417AA2D131A0600B4F2D2 /* SubPages */, + 132417B02D131A0600B4F2D2 /* SettingView.swift */, + ); + path = SettingsViews; + sourceTree = ""; + }; + 132417AA2D131A0600B4F2D2 /* SubPages */ = { + isa = PBXGroup; + children = ( + 132417AB2D131A0600B4F2D2 /* SettingsAboutView.swift */, + 132417AC2D131A0600B4F2D2 /* SettingsIUView.swift */, + 132417AD2D131A0600B4F2D2 /* SettingsLogsView.swift */, + 132417AE2D131A0600B4F2D2 /* SettingsModuleView.swift */, + 132417AF2D131A0600B4F2D2 /* SettingsPlayerView.swift */, + ); + path = SubPages; + sourceTree = ""; + }; + 132417B22D131A0600B4F2D2 /* LibraryViews */ = { + isa = PBXGroup; + children = ( + 132417B32D131A0600B4F2D2 /* LibraryManager.swift */, + 132417B42D131A0600B4F2D2 /* LibraryView.swift */, + ); + path = LibraryViews; + sourceTree = ""; + }; + 132417B52D131A0600B4F2D2 /* AnimeViews */ = { + isa = PBXGroup; + children = ( + 132417B62D131A0600B4F2D2 /* AnimeInfoView.swift */, + 132417B72D131A0600B4F2D2 /* AnimeInfoExtraction.swift */, + ); + path = AnimeViews; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 1324177F2D13198000B4F2D2 /* Sora */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1324178E2D13198200B4F2D2 /* Build configuration list for PBXNativeTarget "Sora" */; + buildPhases = ( + 1324177C2D13198000B4F2D2 /* Sources */, + 1324177D2D13198000B4F2D2 /* Frameworks */, + 1324177E2D13198000B4F2D2 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sora; + packageProductDependencies = ( + 132417C82D131B0D00B4F2D2 /* Kingfisher */, + 132417CE2D131B7400B4F2D2 /* SwiftSoup */, + ); + productName = Sora; + productReference = 132417802D13198000B4F2D2 /* Sora.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 132417782D13198000B4F2D2 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1320; + LastUpgradeCheck = 1320; + TargetAttributes = { + 1324177F2D13198000B4F2D2 = { + CreatedOnToolsVersion = 13.2.1; + }; + }; + }; + buildConfigurationList = 1324177B2D13198000B4F2D2 /* Build configuration list for PBXProject "Sora" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 132417772D13198000B4F2D2; + packageReferences = ( + 132417C72D131B0D00B4F2D2 /* XCRemoteSwiftPackageReference "Kingfisher" */, + 132417CD2D131B7400B4F2D2 /* XCRemoteSwiftPackageReference "SwiftSoup" */, + ); + productRefGroup = 132417812D13198000B4F2D2 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 1324177F2D13198000B4F2D2 /* Sora */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 1324177E2D13198000B4F2D2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1324178B2D13198200B4F2D2 /* Preview Assets.xcassets in Resources */, + 132417882D13198200B4F2D2 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 1324177C2D13198000B4F2D2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 132417BB2D131A0600B4F2D2 /* SettingsIUView.swift in Sources */, + 132417C42D131A0600B4F2D2 /* AnimeInfoExtraction.swift in Sources */, + 132417B82D131A0600B4F2D2 /* SearchView.swift in Sources */, + 132417A42D1319E800B4F2D2 /* PlayerView.swift in Sources */, + 1324179F2D1319E800B4F2D2 /* Notification.swift in Sources */, + 132417BD2D131A0600B4F2D2 /* SettingsModuleView.swift in Sources */, + 132417BC2D131A0600B4F2D2 /* SettingsLogsView.swift in Sources */, + 132417A22D1319E800B4F2D2 /* ModulesManager.swift in Sources */, + 132417862D13198000B4F2D2 /* ContentView.swift in Sources */, + 132417C22D131A0600B4F2D2 /* LibraryView.swift in Sources */, + 132417A32D1319E800B4F2D2 /* NormalPlayer.swift in Sources */, + 132417C02D131A0600B4F2D2 /* HomeView.swift in Sources */, + 132417BF2D131A0600B4F2D2 /* SettingView.swift in Sources */, + 132417C32D131A0600B4F2D2 /* AnimeInfoView.swift in Sources */, + 132417A12D1319E800B4F2D2 /* ModuleStruct.swift in Sources */, + 132417B92D131A0600B4F2D2 /* SearchResultsView.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 */, + 132417A02D1319E800B4F2D2 /* HistoryManager.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 1324178C2D13198200B4F2D2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 1324178D2D13198200B4F2D2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 1324178F2D13198200B4F2D2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\""; + DEVELOPMENT_TEAM = 399LMK6Q2Y; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = me.cranci.Sora; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 132417902D13198200B4F2D2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\""; + DEVELOPMENT_TEAM = 399LMK6Q2Y; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = me.cranci.Sora; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1324177B2D13198000B4F2D2 /* Build configuration list for PBXProject "Sora" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1324178C2D13198200B4F2D2 /* Debug */, + 1324178D2D13198200B4F2D2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1324178E2D13198200B4F2D2 /* Build configuration list for PBXNativeTarget "Sora" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1324178F2D13198200B4F2D2 /* Debug */, + 132417902D13198200B4F2D2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 132417C72D131B0D00B4F2D2 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/onevcat/Kingfisher.git"; + requirement = { + kind = exactVersion; + version = 7.9.1; + }; + }; + 132417CD2D131B7400B4F2D2 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/scinfu/SwiftSoup.git"; + requirement = { + kind = exactVersion; + version = 2.4.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 132417C82D131B0D00B4F2D2 /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = 132417C72D131B0D00B4F2D2 /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; + 132417CE2D131B7400B4F2D2 /* SwiftSoup */ = { + isa = XCSwiftPackageProductDependency; + package = 132417CD2D131B7400B4F2D2 /* XCRemoteSwiftPackageReference "SwiftSoup" */; + productName = SwiftSoup; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 132417782D13198000B4F2D2 /* Project object */; +} diff --git a/Sora.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Sora.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Sora.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Sora.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Sora.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Sora.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Sora.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Sora.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..9a9f027 --- /dev/null +++ b/Sora.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,25 @@ +{ + "object": { + "pins": [ + { + "package": "Kingfisher", + "repositoryURL": "https://github.com/onevcat/Kingfisher.git", + "state": { + "branch": null, + "revision": "b6f62758f21a8c03cd64f4009c037cfa580a256e", + "version": "7.9.1" + } + }, + { + "package": "SwiftSoup", + "repositoryURL": "https://github.com/scinfu/SwiftSoup.git", + "state": { + "branch": null, + "revision": "5386dab25134eec11fc35fc5e43caf422fad0270", + "version": "2.4.0" + } + } + ] + }, + "version": 1 +} diff --git a/Sora.xcodeproj/project.xcworkspace/xcuserdata/Francesco.xcuserdatad/UserInterfaceState.xcuserstate b/Sora.xcodeproj/project.xcworkspace/xcuserdata/Francesco.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..83dd62f Binary files /dev/null and b/Sora.xcodeproj/project.xcworkspace/xcuserdata/Francesco.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Sora.xcodeproj/xcuserdata/Francesco.xcuserdatad/xcschemes/xcschememanagement.plist b/Sora.xcodeproj/xcuserdata/Francesco.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..be54ba2 --- /dev/null +++ b/Sora.xcodeproj/xcuserdata/Francesco.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + Sora.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/Sora/Assets.xcassets/AccentColor.colorset/.DS_Store b/Sora/Assets.xcassets/AccentColor.colorset/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/Sora/Assets.xcassets/AccentColor.colorset/.DS_Store differ diff --git a/Sora/Assets.xcassets/AccentColor.colorset/Contents.json b/Sora/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..bb57667 --- /dev/null +++ b/Sora/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,15 @@ +{ + "colors" : [ + { + "color" : { + "platform" : "universal", + "reference" : "systemMintColor" + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/1024.jpg b/Sora/Assets.xcassets/AppIcon.appiconset/1024.jpg new file mode 100644 index 0000000..072bc96 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon.appiconset/1024.jpg differ diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/120-1.jpg b/Sora/Assets.xcassets/AppIcon.appiconset/120-1.jpg new file mode 100644 index 0000000..4f038a2 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon.appiconset/120-1.jpg differ diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/120.jpg b/Sora/Assets.xcassets/AppIcon.appiconset/120.jpg new file mode 100644 index 0000000..4f038a2 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon.appiconset/120.jpg differ diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/152.jpg b/Sora/Assets.xcassets/AppIcon.appiconset/152.jpg new file mode 100644 index 0000000..0076f0c Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon.appiconset/152.jpg differ diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/167.jpg b/Sora/Assets.xcassets/AppIcon.appiconset/167.jpg new file mode 100644 index 0000000..f6ab53c Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon.appiconset/167.jpg differ diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/180.jpg b/Sora/Assets.xcassets/AppIcon.appiconset/180.jpg new file mode 100644 index 0000000..748db74 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon.appiconset/180.jpg differ diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/20.jpg b/Sora/Assets.xcassets/AppIcon.appiconset/20.jpg new file mode 100644 index 0000000..6ead676 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon.appiconset/20.jpg differ diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/29.jpg b/Sora/Assets.xcassets/AppIcon.appiconset/29.jpg new file mode 100644 index 0000000..841f216 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon.appiconset/29.jpg differ diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/40-1.jpg b/Sora/Assets.xcassets/AppIcon.appiconset/40-1.jpg new file mode 100644 index 0000000..22e2a31 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon.appiconset/40-1.jpg differ diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/40-2.jpg b/Sora/Assets.xcassets/AppIcon.appiconset/40-2.jpg new file mode 100644 index 0000000..22e2a31 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon.appiconset/40-2.jpg differ diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/40.jpg b/Sora/Assets.xcassets/AppIcon.appiconset/40.jpg new file mode 100644 index 0000000..22e2a31 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon.appiconset/40.jpg differ diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/58-1.jpg b/Sora/Assets.xcassets/AppIcon.appiconset/58-1.jpg new file mode 100644 index 0000000..2dc5162 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon.appiconset/58-1.jpg differ diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/58.jpg b/Sora/Assets.xcassets/AppIcon.appiconset/58.jpg new file mode 100644 index 0000000..2dc5162 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon.appiconset/58.jpg differ diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/60.jpg b/Sora/Assets.xcassets/AppIcon.appiconset/60.jpg new file mode 100644 index 0000000..4aa6852 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon.appiconset/60.jpg differ diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/76.jpg b/Sora/Assets.xcassets/AppIcon.appiconset/76.jpg new file mode 100644 index 0000000..4fc0966 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon.appiconset/76.jpg differ diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/80-1.jpg b/Sora/Assets.xcassets/AppIcon.appiconset/80-1.jpg new file mode 100644 index 0000000..2a85707 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon.appiconset/80-1.jpg differ diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/80.jpg b/Sora/Assets.xcassets/AppIcon.appiconset/80.jpg new file mode 100644 index 0000000..2a85707 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon.appiconset/80.jpg differ diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/87.jpg b/Sora/Assets.xcassets/AppIcon.appiconset/87.jpg new file mode 100644 index 0000000..5396eeb Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon.appiconset/87.jpg differ diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/Contents.json b/Sora/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..661a179 --- /dev/null +++ b/Sora/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,116 @@ +{ + "images" : [ + { + "filename" : "40.jpg", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "60.jpg", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "58.jpg", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "87.jpg", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "80-1.jpg", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "120-1.jpg", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "120.jpg", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "180.jpg", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "20.jpg", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "40-1.jpg", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "29.jpg", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "58-1.jpg", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "40-2.jpg", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "80.jpg", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "76.jpg", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "152.jpg", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "167.jpg", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "1024.jpg", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sora/Assets.xcassets/Contents.json b/Sora/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Sora/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sora/ContentView.swift b/Sora/ContentView.swift new file mode 100644 index 0000000..eaeea09 --- /dev/null +++ b/Sora/ContentView.swift @@ -0,0 +1,39 @@ +// +// ContentView.swift +// Sora +// +// Created by Francesco on 18/12/24. +// + +import SwiftUI + +struct ContentView: View { + @EnvironmentObject var modulesManager: ModulesManager + + var body: some View { + TabView { + HomeView() + .tabItem { + Label("Home", systemImage: "house") + } + LibraryView() + .tabItem { + Label("Library", systemImage: "books.vertical") + } + SearchView() + .tabItem { + Label("Search", systemImage: "magnifyingglass") + } + SettingsView() + .tabItem { + Label("Settings", systemImage: "gearshape") + } + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/Sora/Info.plist b/Sora/Info.plist new file mode 100644 index 0000000..e46be83 --- /dev/null +++ b/Sora/Info.plist @@ -0,0 +1,19 @@ + + + + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + me.cranci.scheme + CFBundleURLSchemes + + ryu + + + + + diff --git a/Sora/Preview Content/Preview Assets.xcassets/Contents.json b/Sora/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Sora/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sora/SoraApp.swift b/Sora/SoraApp.swift new file mode 100644 index 0000000..71f156b --- /dev/null +++ b/Sora/SoraApp.swift @@ -0,0 +1,49 @@ +// +// SoraApp.swift +// Sora +// +// Created by Francesco on 18/12/24. +// + +import SwiftUI + +@main +struct SoraApp: App { + @StateObject private var settings = Settings() + @StateObject private var modulesManager = ModulesManager() + + var body: some Scene { + WindowGroup { + ContentView() + .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 new file mode 100644 index 0000000..44863ae --- /dev/null +++ b/Sora/Utils/Extensions/Notification.swift @@ -0,0 +1,13 @@ +// +// 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/History/HistoryManager.swift b/Sora/Utils/History/HistoryManager.swift new file mode 100644 index 0000000..dd353fd --- /dev/null +++ b/Sora/Utils/History/HistoryManager.swift @@ -0,0 +1,35 @@ +// +// 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 + 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/Miru/MiruDataStruct.swift b/Sora/Utils/Miru/MiruDataStruct.swift new file mode 100644 index 0000000..8b42d02 --- /dev/null +++ b/Sora/Utils/Miru/MiruDataStruct.swift @@ -0,0 +1,26 @@ +// +// MiruDataStruct.swift +// Sora +// +// Created by Francesco on 18/12/24. +// + +import Foundation + +struct MiruDataStruct: Codable { + let likes: [Like] + + struct Like: Codable { + let anilistID: Int + let gogoSlug: String + let title: String + let cover: String + + enum CodingKeys: String, CodingKey { + case anilistID = "anilist_id" + case gogoSlug = "gogo_slug" + case title + case cover + } + } +} diff --git a/Sora/Utils/Modules/ModuleStruct.swift b/Sora/Utils/Modules/ModuleStruct.swift new file mode 100644 index 0000000..20a05b7 --- /dev/null +++ b/Sora/Utils/Modules/ModuleStruct.swift @@ -0,0 +1,76 @@ +// +// 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 + + 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 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 + } + } +} diff --git a/Sora/Utils/Modules/ModulesManager.swift b/Sora/Utils/Modules/ModulesManager.swift new file mode 100644 index 0000000..3240c17 --- /dev/null +++ b/Sora/Utils/Modules/ModulesManager.swift @@ -0,0 +1,153 @@ +// +// 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.shared.dataTask(with: url) { data, response, 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() + 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() + } + } + + func refreshModules() { + for (name, urlString) in moduleURLs { + guard let url = URL(string: urlString) else { continue } + let task = URLSession.shared.dataTask(with: url) { data, response, 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)") + } + } + + private 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/Utils/Player/NormalPlayer.swift b/Sora/Utils/Player/NormalPlayer.swift new file mode 100644 index 0000000..1f121d3 --- /dev/null +++ b/Sora/Utils/Player/NormalPlayer.swift @@ -0,0 +1,70 @@ +// +// NormalPlayer.swift +// Sora +// +// Created by Francesco on 18/12/24. +// + +import AVKit + +class NormalPlayer: AVPlayerViewController { + private var originalRate: Float = 1.0 + private var holdGesture: UILongPressGestureRecognizer? + + override func viewDidLoad() { + super.viewDidLoad() + setupHoldGesture() + setupAudioSession() + } + + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + if UserDefaults.standard.bool(forKey: "alwaysLandscape") { + return .landscape + } else { + return .all + } + } + + private func setupHoldGesture() { + holdGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleHoldGesture(_:))) + holdGesture?.minimumPressDuration = 0.5 + if let holdGesture = holdGesture { + view.addGestureRecognizer(holdGesture) + } + } + + @objc private func handleHoldGesture(_ gesture: UILongPressGestureRecognizer) { + switch gesture.state { + case .began: + beginHoldSpeed() + case .ended, .cancelled: + endHoldSpeed() + default: + break + } + } + + private func beginHoldSpeed() { + guard let player = player else { return } + originalRate = player.rate + let holdSpeed = UserDefaults.standard.float(forKey: "holdSpeedPlayer") + player.rate = holdSpeed + } + + private func endHoldSpeed() { + player?.rate = originalRate + } + + func setupAudioSession() { + do { + let audioSession = AVAudioSession.sharedInstance() + try audioSession.setCategory(.playback, mode: .moviePlayback, options: .mixWithOthers) + try audioSession.setActive(true) + + try audioSession.overrideOutputAudioPort(.speaker) + } catch { + print("Failed to set up AVAudioSession: \(error)") + } + } +} + diff --git a/Sora/Utils/Player/PlayerView.swift b/Sora/Utils/Player/PlayerView.swift new file mode 100644 index 0000000..1079c43 --- /dev/null +++ b/Sora/Utils/Player/PlayerView.swift @@ -0,0 +1,40 @@ +// +// PlayerView.swift +// Sora +// +// Created by Francesco on 18/12/24. +// + +import UIKit +import AVKit + +class VideoPlayerViewController: UIViewController { + var player: AVPlayer? + var playerViewController: AVPlayerViewController? + + var streamUrl: String? + + override func viewDidLoad() { + super.viewDidLoad() + + guard let streamUrl = streamUrl, let url = URL(string: streamUrl) else { + return + } + + player = AVPlayer(url: url) + playerViewController = AVPlayerViewController() + playerViewController?.player = player + + if let playerViewController = playerViewController { + playerViewController.view.frame = self.view.frame + self.view.addSubview(playerViewController.view) + self.addChild(playerViewController) + playerViewController.didMove(toParent: self) + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + player?.play() + } +} diff --git a/Sora/Views/AnimeViews/AnimeInfoExtraction.swift b/Sora/Views/AnimeViews/AnimeInfoExtraction.swift new file mode 100644 index 0000000..4ad8598 --- /dev/null +++ b/Sora/Views/AnimeViews/AnimeInfoExtraction.swift @@ -0,0 +1,166 @@ +// +// AnimeInfoExtraction.swift +// Sora +// +// Created by Francesco on 18/12/24. +// + +import SwiftUI +import SwiftSoup + +extension AnimeInfoView { + func fetchAnimeDetails() { + guard let url = URL(string: anime.href.hasPrefix("https") ? anime.href : "\(module.module[0].details.baseURL)\(anime.href)") else { return } + + URLSession.shared.dataTask(with: url) { data, response, 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() + } + + func extractStreamURLs(from html: String, streamType: String) -> [String] { + let pattern: String + switch streamType { + case "HLS": + pattern = #"https:\/\/[^"\s<>]+\.m3u8(?:\?[^\s"'<>]+)?"# + case "MP4": + pattern = #"https:\/\/(?:(?!php).)+\.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 extractDubSubURLs(from htmlContent: String) -> [(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 + } + } + + func fetchEpisodeStream(urlString: String) { + guard let url = URL(string: urlString) else { return } + + Logger.shared.log("Pressed episode button") + URLSession.shared.dataTask(with: url) { data, response, 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) + } + } else { + DispatchQueue.main.async { + self.playStream(urlString: streamURLs.first, fullURL: urlString) + } + } + } else { + DispatchQueue.main.async { + Logger.shared.log("stream URLs: \(streamURLs)") + self.playStream(urlString: streamURLs.first, fullURL: urlString) + } + } + }.resume() + } + + func presentStreamSelection(subURLs: [String], dubURLs: [String]) { + let uniqueSubURLs = Array(Set(subURLs)) + let uniqueDubURLs = Array(Set(dubURLs)) + + 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: dubURL) + }) + } + } + + if !uniqueSubURLs.isEmpty { + for subURL in uniqueSubURLs { + alert.addAction(UIAlertAction(title: "SUB", style: .default) { _ in + self.playStream(urlString: subURL, fullURL: subURL) + }) + } + } + + 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 { + rootVC.present(alert, animated: true, completion: nil) + } + } + } +} diff --git a/Sora/Views/AnimeViews/AnimeInfoView.swift b/Sora/Views/AnimeViews/AnimeInfoView.swift new file mode 100644 index 0000000..8bb105e --- /dev/null +++ b/Sora/Views/AnimeViews/AnimeInfoView.swift @@ -0,0 +1,225 @@ +// +// AnimeDetailsView.swift +// Sora +// +// Created by Francesco on 18/12/24. +// + +import AVKit +import SwiftUI +import SwiftSoup +import Kingfisher +import SafariServices + +struct EpisodeCell: View { + let episode: String + let episodeID: Int + let imageUrl: String + + var body: some View { + HStack { + KFImage(URL(string: "https://cdn.discordapp.com/attachments/1218851049625092138/1318941731349332029/IMG_5081.png?ex=676427b5&is=6762d635&hm=923252d3448fda337f52c964f1428538095cbd018e36a6cfb21d01918e071c9d&")) + .resizable() + .aspectRatio(16/9, contentMode: .fill) + .frame(width: 100, height: 56) + .cornerRadius(8) + + VStack(alignment: .leading) { + Text("Episode \(episodeID + 1)") + .font(.headline) + } + } + } +} + +struct AnimeInfoView: View { + let module: ModuleStruct + let anime: SearchResult + + @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 + + @AppStorage("externalPlayer") private var externalPlayer: String = "default" + + var body: some View { + VStack { + if isLoading { + ProgressView() + .padding() + } else { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + HStack(alignment: .top, spacing: 10) { + KFImage(URL(string: anime.imageUrl)) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 190) + .cornerRadius(10) + + VStack(alignment: .leading, spacing: 4) { + Text(anime.name) + .font(.system(size: 17)) + .fontWeight(.bold) + + if !aliases.isEmpty && aliases != anime.name { + Text(aliases) + .font(.system(size: 13)) + .foregroundColor(.secondary) + } + + Spacer() + + HStack(alignment: .top, 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: { + }) { + 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 !episodes.isEmpty { + VStack(alignment: .leading, spacing: 10) { + Text("Episodes") + .font(.system(size: 18)) + .fontWeight(.bold) + + ForEach(episodes.indices, id: \.self) { index in + EpisodeCell(episode: episodes[index], episodeID: index, imageUrl: anime.imageUrl) + .onTapGesture { + fetchEpisodeStream(urlString: "\(module.module[0].details.baseURL)\(episodes[index])") + } + } + } + } + } + .padding() + } + } + } + .onAppear { + fetchAnimeDetails() + } + } + + func playStream(urlString: String?, fullURL: String) { + guard let streamUrl = urlString else { return } + + if externalPlayer != "Default" { + 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 = "\(externalPlayer)://" + } + openInExternalPlayer(scheme: scheme, url: streamUrl) + return + } + + DispatchQueue.main.async { + let videoPlayerViewController = VideoPlayerViewController() + videoPlayerViewController.streamUrl = streamUrl + 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: anime.href.hasPrefix("http") ? anime.href : "\(module.module[0].details.baseURL)\(anime.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'") + } +} diff --git a/Sora/Views/HomeView.swift b/Sora/Views/HomeView.swift new file mode 100644 index 0000000..110daa3 --- /dev/null +++ b/Sora/Views/HomeView.swift @@ -0,0 +1,15 @@ +// +// HomeView.swift +// Sora +// +// Created by Francesco on 18/12/24. +// + +import SwiftUI + +struct HomeView: View { + var body: some View { + Text("Home View") + .navigationTitle("Home") + } +} diff --git a/Sora/Views/LibraryViews/LibraryManager.swift b/Sora/Views/LibraryViews/LibraryManager.swift new file mode 100644 index 0000000..37aa7ea --- /dev/null +++ b/Sora/Views/LibraryViews/LibraryManager.swift @@ -0,0 +1,69 @@ +// +// 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.anilistID == item.anilistID }) { + 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) { + var newLibraryItems: [LibraryItem] = [] + + for like in miruData.likes { + let libraryItem = LibraryItem( + anilistID: like.anilistID, + title: like.title, + image: like.cover, + url: like.gogoSlug, + dateAdded: Date() + ) + newLibraryItems.append(libraryItem) + Logger.shared.log("Importing item: \(libraryItem.title)") + } + + DispatchQueue.main.async { + self.libraryItems = 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 new file mode 100644 index 0000000..94a21a3 --- /dev/null +++ b/Sora/Views/LibraryViews/LibraryView.swift @@ -0,0 +1,81 @@ +// +// 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 + 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 + itemView(item) + } + } + .padding() + } + } + .navigationTitle("Library") + } + } + + 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 { + KFImage(URL(string: item.image)) + .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) + } + .contextMenu { + Button(role: .destructive) { + libraryManager.removeFromLibrary(item) + } label: { + Label("Remove from Library", systemImage: "trash") + } + } + } +} diff --git a/Sora/Views/SearchViews/SearchResultsView.swift b/Sora/Views/SearchViews/SearchResultsView.swift new file mode 100644 index 0000000..4e8d357 --- /dev/null +++ b/Sora/Views/SearchViews/SearchResultsView.swift @@ -0,0 +1,210 @@ +// +// SearchResultsView.swift +// Sora +// +// Created by Francesco on 18/12/24. +// + +import SwiftUI +import Kingfisher +import SwiftSoup + +struct SearchResultsView: View { + let module: ModuleStruct? + let searchText: String + @State private var searchResults: [SearchResult] = [] + @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: AnimeInfoView(module: module!, anime: 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() + } + .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: AnimeInfoView(module: module!, anime: 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 + } + } + } + .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: [SearchResult] { + 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 urlString = "\(module.module[0].search.url)?\(module.module[0].search.parameter)=\(encodedSearchText)" + guard let url = URL(string: urlString) else { return } + + URLSession.shared.dataTask(with: url) { data, response, 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: [SearchResult] = [] + 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.starts(with: "http") { + imageURL = "\(module.module[0].details.baseURL)\(imageURL)" + } + + let result = SearchResult(name: title, imageUrl: imageURL, href: href) + results.append(result) + } + + 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 new file mode 100644 index 0000000..f96b164 --- /dev/null +++ b/Sora/Views/SearchViews/SearchView.swift @@ -0,0 +1,147 @@ +// +// SearchView.swift +// Sora +// +// Created by Francesco on 18/12/24. +// + +import SwiftUI + +struct SearchResult: 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: [SearchResult] = [] + @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) + } + } + .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 or add it in settings."), + dismissButton: .default(Text("OK")) + ) + } + } + } +} + +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/SettingsViews/SettingView.swift b/Sora/Views/SettingsViews/SettingView.swift new file mode 100644 index 0000000..7950eec --- /dev/null +++ b/Sora/Views/SettingsViews/SettingView.swift @@ -0,0 +1,276 @@ +// +// 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 = "" + @StateObject private var libraryManager = LibraryManager.shared + + 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("Minor Interface Settings") + } + NavigationLink(destination: SettingsPlayerView()) { + HStack { + Image(systemName: "play.fill") + Text("Player") + } + } + } + + Section(header: Text("External Features")) { + NavigationLink(destination: SettingsModuleView()) { + HStack { + Image(systemName: "puzzlepiece.fill") + Text("Modules") + } + } + Button(action: { + isDocumentPickerPresented = true + }) { + HStack { + Image(systemName: "tray.and.arrow.down.fill") + Text("Import Miru Bookmarks") + } + } + } + + Section(header: Text("Debug")) { + NavigationLink(destination: SettingsLogsView()) { + HStack { + Image(systemName: "doc.text.fill") + Text("Logs") + } + } + } + + Section(header: Text("Info")) { + NavigationLink(destination: AboutView()) { + HStack { + Text("About") + } + } + 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: { + 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) + } + } + } +} + +struct DocumentPicker: UIViewControllerRepresentable { + var libraryManager: LibraryManager + var onSuccess: () -> 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: () -> Void + var onFailure: (String) -> Void + + init(_ parent: DocumentPicker, libraryManager: LibraryManager, onSuccess: @escaping () -> 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) + let miruData = try JSONDecoder().decode(MiruDataStruct.self, from: data) + libraryManager.importFromMiruData(miruData) + Logger.shared.log("Imported Miru data from \(selectedFileURL)") + onSuccess() + } 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 cancelled" + 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 new file mode 100644 index 0000000..5ae4c79 --- /dev/null +++ b/Sora/Views/SettingsViews/SubPages/SettingsAboutView.swift @@ -0,0 +1,149 @@ +// +// 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/1024.jpg")) + .resizable() + .frame(width: 80, height: 80) + .clipShape(Circle()) + + VStack(alignment: .leading) { + Text("Sora") + .font(.largeTitle) + .fontWeight(.bold) + + Text("Version 1.0.0") + .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(.yellow) + Text("YAY it's me") + .font(.subheadline) + .foregroundColor(.secondary) + } + Spacer() + Image(systemName: "safari") + .foregroundColor(.yellow) + } + } + } + + 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 <3")) { + 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) + } + } + } + + 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) + } + } + } + } + .navigationTitle("About") + } +} diff --git a/Sora/Views/SettingsViews/SubPages/SettingsIUView.swift b/Sora/Views/SettingsViews/SubPages/SettingsIUView.swift new file mode 100644 index 0000000..24953b8 --- /dev/null +++ b/Sora/Views/SettingsViews/SubPages/SettingsIUView.swift @@ -0,0 +1,22 @@ +// +// 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 new file mode 100644 index 0000000..fc961bc --- /dev/null +++ b/Sora/Views/SettingsViews/SubPages/SettingsLogsView.swift @@ -0,0 +1,72 @@ +// +// 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 new file mode 100644 index 0000000..c4fc85b --- /dev/null +++ b/Sora/Views/SettingsViews/SubPages/SettingsModuleView.swift @@ -0,0 +1,126 @@ +// +// 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? + + 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) { + 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://cranci.tech/sora/animeworld.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 new file mode 100644 index 0000000..9c6c8c9 --- /dev/null +++ b/Sora/Views/SettingsViews/SubPages/SettingsPlayerView.swift @@ -0,0 +1,58 @@ +// +// 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("holdSpeedPlayer") private var holdSpeedPlayer: Double = 2.0 + + var body: some View { + Form { + Section(header: Text("Player"), footer: Text("The ForceLandscape and HoldSpeed only work inside the default iOS player.")) { + HStack { + Text("Media Player") + Spacer() + Menu(externalPlayer) { + Button("Default") { + externalPlayer = "Default" + } + Button("VLC") { + externalPlayer = "VLC" + } + Button("OutPlayer") { + externalPlayer = "OutPlayer" + } + Button("Infuse") { + externalPlayer = "Infuse" + } + Button("nPlayer") { + externalPlayer = "nPlayer" + } + } + } + + Toggle("Force Landscape", isOn: $isAlwaysLandscape) + + 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/ipabuild.sh b/ipabuild.sh new file mode 100755 index 0000000..e9ea9bd --- /dev/null +++ b/ipabuild.sh @@ -0,0 +1,42 @@ +#!/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 'generic/platform=iOS' \ + 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-iphoneos/$APPLICATION_NAME.app" +TARGET_APP="$WORKING_LOCATION/build/$APPLICATION_NAME.app" +cp -r "$DD_APP_PATH" "$TARGET_APP" + +codesign --remove "$TARGET_APP" +if [ -e "$TARGET_APP/_CodeSignature" ]; then + rm -rf "$TARGET_APP/_CodeSignature" +fi +if [ -e "$TARGET_APP/embedded.mobileprovision" ]; then + rm -rf "$TARGET_APP/embedded.mobileprovision" +fi + + +mkdir Payload +cp -r Sora.app Payload/Sora.app +strip Payload/Sora.app/Sora +zip -vr Sora.ipa Payload +rm -rf Sora.app +rm -rf Payload