sora
570
Sora.xcodeproj/project.pbxproj
Normal file
|
|
@ -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 = "<group>"; };
|
||||||
|
132417852D13198000B4F2D2 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
|
132417872D13198200B4F2D2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
1324178A2D13198200B4F2D2 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||||
|
132417932D1319E800B4F2D2 /* MiruDataStruct.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MiruDataStruct.swift; sourceTree = "<group>"; };
|
||||||
|
132417952D1319E800B4F2D2 /* Notification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = "<group>"; };
|
||||||
|
132417972D1319E800B4F2D2 /* HistoryManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HistoryManager.swift; sourceTree = "<group>"; };
|
||||||
|
132417992D1319E800B4F2D2 /* ModuleStruct.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModuleStruct.swift; sourceTree = "<group>"; };
|
||||||
|
1324179A2D1319E800B4F2D2 /* ModulesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModulesManager.swift; sourceTree = "<group>"; };
|
||||||
|
1324179C2D1319E800B4F2D2 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = "<group>"; };
|
||||||
|
1324179D2D1319E800B4F2D2 /* PlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = "<group>"; };
|
||||||
|
132417A72D131A0600B4F2D2 /* SearchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
|
||||||
|
132417A82D131A0600B4F2D2 /* SearchResultsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = "<group>"; };
|
||||||
|
132417AB2D131A0600B4F2D2 /* SettingsAboutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAboutView.swift; sourceTree = "<group>"; };
|
||||||
|
132417AC2D131A0600B4F2D2 /* SettingsIUView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsIUView.swift; sourceTree = "<group>"; };
|
||||||
|
132417AD2D131A0600B4F2D2 /* SettingsLogsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsLogsView.swift; sourceTree = "<group>"; };
|
||||||
|
132417AE2D131A0600B4F2D2 /* SettingsModuleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsModuleView.swift; sourceTree = "<group>"; };
|
||||||
|
132417AF2D131A0600B4F2D2 /* SettingsPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsPlayerView.swift; sourceTree = "<group>"; };
|
||||||
|
132417B02D131A0600B4F2D2 /* SettingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingView.swift; sourceTree = "<group>"; };
|
||||||
|
132417B12D131A0600B4F2D2 /* HomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
|
||||||
|
132417B32D131A0600B4F2D2 /* LibraryManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryManager.swift; sourceTree = "<group>"; };
|
||||||
|
132417B42D131A0600B4F2D2 /* LibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = "<group>"; };
|
||||||
|
132417B62D131A0600B4F2D2 /* AnimeInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimeInfoView.swift; sourceTree = "<group>"; };
|
||||||
|
132417B72D131A0600B4F2D2 /* AnimeInfoExtraction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimeInfoExtraction.swift; sourceTree = "<group>"; };
|
||||||
|
132417C52D131AA500B4F2D2 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
/* 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 = "<group>";
|
||||||
|
};
|
||||||
|
132417812D13198000B4F2D2 /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
132417802D13198000B4F2D2 /* Sora.app */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
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 = "<group>";
|
||||||
|
};
|
||||||
|
132417892D13198200B4F2D2 /* Preview Content */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1324178A2D13198200B4F2D2 /* Preview Assets.xcassets */,
|
||||||
|
);
|
||||||
|
path = "Preview Content";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
132417912D1319E800B4F2D2 /* Utils */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
132417922D1319E800B4F2D2 /* Miru */,
|
||||||
|
132417942D1319E800B4F2D2 /* Extensions */,
|
||||||
|
132417962D1319E800B4F2D2 /* History */,
|
||||||
|
132417982D1319E800B4F2D2 /* Modules */,
|
||||||
|
1324179B2D1319E800B4F2D2 /* Player */,
|
||||||
|
);
|
||||||
|
path = Utils;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
132417922D1319E800B4F2D2 /* Miru */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
132417932D1319E800B4F2D2 /* MiruDataStruct.swift */,
|
||||||
|
);
|
||||||
|
path = Miru;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
132417942D1319E800B4F2D2 /* Extensions */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
132417952D1319E800B4F2D2 /* Notification.swift */,
|
||||||
|
);
|
||||||
|
path = Extensions;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
132417962D1319E800B4F2D2 /* History */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
132417972D1319E800B4F2D2 /* HistoryManager.swift */,
|
||||||
|
);
|
||||||
|
path = History;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
132417982D1319E800B4F2D2 /* Modules */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
132417992D1319E800B4F2D2 /* ModuleStruct.swift */,
|
||||||
|
1324179A2D1319E800B4F2D2 /* ModulesManager.swift */,
|
||||||
|
);
|
||||||
|
path = Modules;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1324179B2D1319E800B4F2D2 /* Player */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1324179C2D1319E800B4F2D2 /* NormalPlayer.swift */,
|
||||||
|
1324179D2D1319E800B4F2D2 /* PlayerView.swift */,
|
||||||
|
);
|
||||||
|
path = Player;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
132417A52D131A0600B4F2D2 /* Views */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
132417A62D131A0600B4F2D2 /* SearchViews */,
|
||||||
|
132417A92D131A0600B4F2D2 /* SettingsViews */,
|
||||||
|
132417B12D131A0600B4F2D2 /* HomeView.swift */,
|
||||||
|
132417B22D131A0600B4F2D2 /* LibraryViews */,
|
||||||
|
132417B52D131A0600B4F2D2 /* AnimeViews */,
|
||||||
|
);
|
||||||
|
path = Views;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
132417A62D131A0600B4F2D2 /* SearchViews */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
132417A72D131A0600B4F2D2 /* SearchView.swift */,
|
||||||
|
132417A82D131A0600B4F2D2 /* SearchResultsView.swift */,
|
||||||
|
);
|
||||||
|
path = SearchViews;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
132417A92D131A0600B4F2D2 /* SettingsViews */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
132417AA2D131A0600B4F2D2 /* SubPages */,
|
||||||
|
132417B02D131A0600B4F2D2 /* SettingView.swift */,
|
||||||
|
);
|
||||||
|
path = SettingsViews;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
132417AA2D131A0600B4F2D2 /* SubPages */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
132417AB2D131A0600B4F2D2 /* SettingsAboutView.swift */,
|
||||||
|
132417AC2D131A0600B4F2D2 /* SettingsIUView.swift */,
|
||||||
|
132417AD2D131A0600B4F2D2 /* SettingsLogsView.swift */,
|
||||||
|
132417AE2D131A0600B4F2D2 /* SettingsModuleView.swift */,
|
||||||
|
132417AF2D131A0600B4F2D2 /* SettingsPlayerView.swift */,
|
||||||
|
);
|
||||||
|
path = SubPages;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
132417B22D131A0600B4F2D2 /* LibraryViews */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
132417B32D131A0600B4F2D2 /* LibraryManager.swift */,
|
||||||
|
132417B42D131A0600B4F2D2 /* LibraryView.swift */,
|
||||||
|
);
|
||||||
|
path = LibraryViews;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
132417B52D131A0600B4F2D2 /* AnimeViews */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
132417B62D131A0600B4F2D2 /* AnimeInfoView.swift */,
|
||||||
|
132417B72D131A0600B4F2D2 /* AnimeInfoExtraction.swift */,
|
||||||
|
);
|
||||||
|
path = AnimeViews;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* 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 */;
|
||||||
|
}
|
||||||
7
Sora.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
BIN
Sora.xcodeproj/project.xcworkspace/xcuserdata/Francesco.xcuserdatad/UserInterfaceState.xcuserstate
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>SchemeUserState</key>
|
||||||
|
<dict>
|
||||||
|
<key>Sora.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>0</integer>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
Sora/Assets.xcassets/AccentColor.colorset/.DS_Store
vendored
Normal file
15
Sora/Assets.xcassets/AccentColor.colorset/Contents.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"platform" : "universal",
|
||||||
|
"reference" : "systemMintColor"
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Sora/Assets.xcassets/AppIcon.appiconset/1024.jpg
Normal file
|
After Width: | Height: | Size: 429 KiB |
BIN
Sora/Assets.xcassets/AppIcon.appiconset/120-1.jpg
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
Sora/Assets.xcassets/AppIcon.appiconset/120.jpg
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
Sora/Assets.xcassets/AppIcon.appiconset/152.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
Sora/Assets.xcassets/AppIcon.appiconset/167.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
Sora/Assets.xcassets/AppIcon.appiconset/180.jpg
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
Sora/Assets.xcassets/AppIcon.appiconset/20.jpg
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
Sora/Assets.xcassets/AppIcon.appiconset/29.jpg
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
Sora/Assets.xcassets/AppIcon.appiconset/40-1.jpg
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
Sora/Assets.xcassets/AppIcon.appiconset/40-2.jpg
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
Sora/Assets.xcassets/AppIcon.appiconset/40.jpg
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
Sora/Assets.xcassets/AppIcon.appiconset/58-1.jpg
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
Sora/Assets.xcassets/AppIcon.appiconset/58.jpg
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
Sora/Assets.xcassets/AppIcon.appiconset/60.jpg
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
Sora/Assets.xcassets/AppIcon.appiconset/76.jpg
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
Sora/Assets.xcassets/AppIcon.appiconset/80-1.jpg
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
Sora/Assets.xcassets/AppIcon.appiconset/80.jpg
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
Sora/Assets.xcassets/AppIcon.appiconset/87.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
116
Sora/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
6
Sora/Assets.xcassets/Contents.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Sora/ContentView.swift
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
19
Sora/Info.plist
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>me.cranci.scheme</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>ryu</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
49
Sora/SoraApp.swift
Normal file
|
|
@ -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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
13
Sora/Utils/Extensions/Notification.swift
Normal file
|
|
@ -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")
|
||||||
|
}
|
||||||
35
Sora/Utils/History/HistoryManager.swift
Normal file
|
|
@ -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<AnyCancellable>()
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Sora/Utils/Miru/MiruDataStruct.swift
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
76
Sora/Utils/Modules/ModuleStruct.swift
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
153
Sora/Utils/Modules/ModulesManager.swift
Normal file
|
|
@ -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, Error>) -> 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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
Sora/Utils/Player/NormalPlayer.swift
Normal file
|
|
@ -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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
40
Sora/Utils/Player/PlayerView.swift
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
166
Sora/Views/AnimeViews/AnimeInfoExtraction.swift
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
225
Sora/Views/AnimeViews/AnimeInfoView.swift
Normal file
|
|
@ -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'")
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Sora/Views/HomeView.swift
Normal file
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
69
Sora/Views/LibraryViews/LibraryManager.swift
Normal file
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
Sora/Views/LibraryViews/LibraryView.swift
Normal file
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
210
Sora/Views/SearchViews/SearchResultsView.swift
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
147
Sora/Views/SearchViews/SearchView.swift
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
276
Sora/Views/SettingsViews/SettingView.swift
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
149
Sora/Views/SettingsViews/SubPages/SettingsAboutView.swift
Normal file
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
22
Sora/Views/SettingsViews/SubPages/SettingsIUView.swift
Normal file
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
72
Sora/Views/SettingsViews/SubPages/SettingsLogsView.swift
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
126
Sora/Views/SettingsViews/SubPages/SettingsModuleView.swift
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
Sora/Views/SettingsViews/SubPages/SettingsPlayerView.swift
Normal file
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
42
ipabuild.sh
Executable file
|
|
@ -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
|
||||||