first public commit of Sora-JS 🎱
362
Sora-JS.xcodeproj/project.pbxproj
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 55;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1329D5C72D298198008AEDA2 /* Sora_JSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1329D5C62D298198008AEDA2 /* Sora_JSApp.swift */; };
|
||||
1329D5C92D298198008AEDA2 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1329D5C82D298198008AEDA2 /* ContentView.swift */; };
|
||||
1329D5CB2D298199008AEDA2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1329D5CA2D298199008AEDA2 /* Assets.xcassets */; };
|
||||
1329D5CE2D298199008AEDA2 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1329D5CD2D298199008AEDA2 /* Preview Assets.xcassets */; };
|
||||
13AEE6192D2A75110096D953 /* Modules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13AEE6182D2A75110096D953 /* Modules.swift */; };
|
||||
13AEE61B2D2A78050096D953 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13AEE61A2D2A78050096D953 /* Settings.swift */; };
|
||||
13AEE61D2D2A78160096D953 /* JSController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13AEE61C2D2A78160096D953 /* JSController.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1329D5C32D298198008AEDA2 /* Sora-JS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Sora-JS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
1329D5C62D298198008AEDA2 /* Sora_JSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sora_JSApp.swift; sourceTree = "<group>"; };
|
||||
1329D5C82D298198008AEDA2 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
1329D5CA2D298199008AEDA2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
1329D5CD2D298199008AEDA2 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
1329D5DA2D29821B008AEDA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
13AEE6182D2A75110096D953 /* Modules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modules.swift; sourceTree = "<group>"; };
|
||||
13AEE61A2D2A78050096D953 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
|
||||
13AEE61C2D2A78160096D953 /* JSController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSController.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
1329D5C02D298198008AEDA2 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
1329D5BA2D298198008AEDA2 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1329D5C52D298198008AEDA2 /* Sora-JS */,
|
||||
1329D5C42D298198008AEDA2 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1329D5C42D298198008AEDA2 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1329D5C32D298198008AEDA2 /* Sora-JS.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1329D5C52D298198008AEDA2 /* Sora-JS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1329D5DA2D29821B008AEDA2 /* Info.plist */,
|
||||
1329D5C62D298198008AEDA2 /* Sora_JSApp.swift */,
|
||||
1329D5C82D298198008AEDA2 /* ContentView.swift */,
|
||||
13AEE6182D2A75110096D953 /* Modules.swift */,
|
||||
1329D5CA2D298199008AEDA2 /* Assets.xcassets */,
|
||||
1329D5CC2D298199008AEDA2 /* Preview Content */,
|
||||
13AEE61A2D2A78050096D953 /* Settings.swift */,
|
||||
13AEE61C2D2A78160096D953 /* JSController.swift */,
|
||||
);
|
||||
path = "Sora-JS";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1329D5CC2D298199008AEDA2 /* Preview Content */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1329D5CD2D298199008AEDA2 /* Preview Assets.xcassets */,
|
||||
);
|
||||
path = "Preview Content";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
1329D5C22D298198008AEDA2 /* Sora-JS */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 1329D5D12D298199008AEDA2 /* Build configuration list for PBXNativeTarget "Sora-JS" */;
|
||||
buildPhases = (
|
||||
1329D5BF2D298198008AEDA2 /* Sources */,
|
||||
1329D5C02D298198008AEDA2 /* Frameworks */,
|
||||
1329D5C12D298198008AEDA2 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "Sora-JS";
|
||||
productName = "Sora-JS";
|
||||
productReference = 1329D5C32D298198008AEDA2 /* Sora-JS.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
1329D5BB2D298198008AEDA2 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1320;
|
||||
LastUpgradeCheck = 1320;
|
||||
TargetAttributes = {
|
||||
1329D5C22D298198008AEDA2 = {
|
||||
CreatedOnToolsVersion = 13.2.1;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 1329D5BE2D298198008AEDA2 /* Build configuration list for PBXProject "Sora-JS" */;
|
||||
compatibilityVersion = "Xcode 13.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 1329D5BA2D298198008AEDA2;
|
||||
productRefGroup = 1329D5C42D298198008AEDA2 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
1329D5C22D298198008AEDA2 /* Sora-JS */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
1329D5C12D298198008AEDA2 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1329D5CE2D298199008AEDA2 /* Preview Assets.xcassets in Resources */,
|
||||
1329D5CB2D298199008AEDA2 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
1329D5BF2D298198008AEDA2 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1329D5C92D298198008AEDA2 /* ContentView.swift in Sources */,
|
||||
13AEE61D2D2A78160096D953 /* JSController.swift in Sources */,
|
||||
13AEE6192D2A75110096D953 /* Modules.swift in Sources */,
|
||||
13AEE61B2D2A78050096D953 /* Settings.swift in Sources */,
|
||||
1329D5C72D298198008AEDA2 /* Sora_JSApp.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
1329D5CF2D298199008AEDA2 /* 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.2;
|
||||
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;
|
||||
};
|
||||
1329D5D02D298199008AEDA2 /* 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.2;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
1329D5D22D298199008AEDA2 /* 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-JS/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 399LMK6Q2Y;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Sora-JS/Info.plist";
|
||||
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";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "me.cranci.Sora-JS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
1329D5D32D298199008AEDA2 /* 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-JS/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 399LMK6Q2Y;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Sora-JS/Info.plist";
|
||||
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";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "me.cranci.Sora-JS";
|
||||
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 */
|
||||
1329D5BE2D298198008AEDA2 /* Build configuration list for PBXProject "Sora-JS" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
1329D5CF2D298199008AEDA2 /* Debug */,
|
||||
1329D5D02D298199008AEDA2 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
1329D5D12D298199008AEDA2 /* Build configuration list for PBXNativeTarget "Sora-JS" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
1329D5D22D298199008AEDA2 /* Debug */,
|
||||
1329D5D32D298199008AEDA2 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 1329D5BB2D298198008AEDA2 /* Project object */;
|
||||
}
|
||||
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"platform" : "universal",
|
||||
"reference" : "systemMintColor"
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
|
|
@ -1,109 +1,91 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "40-2.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "60.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "58.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "87.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "80.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "120-1.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "180.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "20.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "40-1.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "29.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "58-1.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "80-1.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "76.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "152.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "167.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename" : "1024.jpg",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
178
Sora-JS/ContentView.swift
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
//
|
||||
// ContentView.swift
|
||||
// Sora-JS
|
||||
//
|
||||
// Created by Francesco on 04/01/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AnimeItem: Identifiable {
|
||||
let id = UUID()
|
||||
let title: String
|
||||
let imageUrl: String
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
@StateObject private var jsController = JSController()
|
||||
@EnvironmentObject var moduleManager: ModuleManager
|
||||
@State private var searchText = ""
|
||||
@State private var animeItems: [AnimeItem] = []
|
||||
@State private var isSearching = false
|
||||
@AppStorage("selectedModuleId") private var selectedModuleId: String?
|
||||
|
||||
private var selectedModule: ScrapingModule? {
|
||||
guard let id = selectedModuleId else { return nil }
|
||||
return moduleManager.modules.first { $0.id.uuidString == id }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack(spacing: 0) {
|
||||
if let selectedModule = selectedModule {
|
||||
HStack {
|
||||
AsyncImage(url: URL(string: selectedModule.metadata.iconUrl)) { image in
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
} placeholder: {
|
||||
Color.gray
|
||||
}
|
||||
.frame(width: 30, height: 30)
|
||||
.cornerRadius(6)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(selectedModule.metadata.mediaType)
|
||||
.font(.headline)
|
||||
Text(selectedModule.metadata.language)
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Menu {
|
||||
ForEach(moduleManager.modules) { module in
|
||||
Button {
|
||||
selectedModuleId = module.id.uuidString
|
||||
} label: {
|
||||
HStack {
|
||||
AsyncImage(url: URL(string: module.metadata.iconUrl)) { image in
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
} placeholder: {
|
||||
Color.gray
|
||||
}
|
||||
.frame(width: 20, height: 20)
|
||||
.cornerRadius(4)
|
||||
|
||||
Text(module.metadata.mediaType)
|
||||
Spacer()
|
||||
if module.id.uuidString == selectedModuleId {
|
||||
Image(systemName: "checkmark")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "chevron.up.chevron.down")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemBackground))
|
||||
.shadow(color: Color.black.opacity(0.1), radius: 2, y: 1)
|
||||
} else {
|
||||
VStack(spacing: 8) {
|
||||
Text("No Module Selected")
|
||||
.font(.headline)
|
||||
Text("Please select a module from settings")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color(.systemBackground))
|
||||
.shadow(color: Color.black.opacity(0.1), radius: 2, y: 1)
|
||||
}
|
||||
|
||||
TextField("Search...", text: $searchText)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.padding()
|
||||
.onChange(of: searchText) { newValue in
|
||||
guard !newValue.isEmpty, let module = selectedModule else {
|
||||
animeItems = []
|
||||
return
|
||||
}
|
||||
|
||||
isSearching = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
Task {
|
||||
do {
|
||||
let jsContent = try moduleManager.getModuleContent(module)
|
||||
jsController.loadScript(jsContent)
|
||||
jsController.scrapeAnime(keyword: newValue, module: module) { items in
|
||||
animeItems = items
|
||||
isSearching = false
|
||||
}
|
||||
} catch {
|
||||
print("Error loading module: \(error)")
|
||||
isSearching = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isSearching {
|
||||
ProgressView()
|
||||
.padding()
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
LazyVGrid(columns: [
|
||||
GridItem(.flexible(), spacing: 16),
|
||||
GridItem(.flexible(), spacing: 16)
|
||||
], spacing: 16) {
|
||||
ForEach(animeItems) { item in
|
||||
VStack(alignment: .leading) {
|
||||
if let url = URL(string: item.imageUrl) {
|
||||
AsyncImage(url: url) { image in
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
} placeholder: {
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
.overlay(
|
||||
ProgressView()
|
||||
)
|
||||
}
|
||||
.frame(height: 200)
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
Text(item.title)
|
||||
.font(.headline)
|
||||
.lineLimit(2)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color(.secondarySystemBackground))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.navigationTitle("Search")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
NavigationLink(destination: SettingsView()) {
|
||||
Image(systemName: "gear")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,10 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
74
Sora-JS/JSController.swift
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
//
|
||||
// JSController.swift
|
||||
// Sora-JS
|
||||
//
|
||||
// Created by Francesco on 05/01/25.
|
||||
//
|
||||
|
||||
import JavaScriptCore
|
||||
|
||||
class JSController: ObservableObject {
|
||||
private var context: JSContext
|
||||
|
||||
init() {
|
||||
self.context = JSContext()
|
||||
setupContext()
|
||||
}
|
||||
|
||||
private func setupContext() {
|
||||
let logFunction: @convention(block) (String) -> Void = { message in
|
||||
print("JavaScript log: \(message)")
|
||||
}
|
||||
context.setObject(logFunction, forKeyedSubscript: "log" as NSString)
|
||||
}
|
||||
|
||||
func loadScript(_ script: String) {
|
||||
context = JSContext()
|
||||
setupContext()
|
||||
context.evaluateScript(script)
|
||||
}
|
||||
|
||||
func scrapeAnime(keyword: String, module: ScrapingModule, completion: @escaping ([AnimeItem]) -> Void) {
|
||||
let searchUrl = module.metadata.searchBaseUrl.replacingOccurrences(of: "%s", with: keyword.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")
|
||||
|
||||
guard let url = URL(string: searchUrl) else {
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36", forHTTPHeaderField: "User-Agent")
|
||||
|
||||
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
|
||||
guard let self = self else { return }
|
||||
|
||||
if let error = error {
|
||||
print("Network error: \(error)")
|
||||
DispatchQueue.main.async { completion([]) }
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let html = String(data: data, encoding: .utf8) else {
|
||||
print("Failed to decode HTML")
|
||||
DispatchQueue.main.async { completion([]) }
|
||||
return
|
||||
}
|
||||
|
||||
if let parseFunction = self.context.objectForKeyedSubscript("parseHTML"),
|
||||
let results = parseFunction.call(withArguments: [html]).toArray() as? [[String: String]] {
|
||||
let animeItems = results.map { item in
|
||||
AnimeItem(
|
||||
title: item["title"] ?? "",
|
||||
imageUrl: item["image"] ?? ""
|
||||
)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
completion(animeItems)
|
||||
}
|
||||
} else {
|
||||
print("Failed to parse results")
|
||||
DispatchQueue.main.async { completion([]) }
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
118
Sora-JS/Modules.swift
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
//
|
||||
// Modules.swift
|
||||
// Sora-JS
|
||||
//
|
||||
// Created by Francesco on 05/01/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ModuleMetadata: Codable, Hashable {
|
||||
let author: String
|
||||
let iconUrl: String
|
||||
let language: String
|
||||
let mediaType: String
|
||||
let searchBaseUrl: String
|
||||
let scriptUrl: String
|
||||
let version: String
|
||||
let description: String
|
||||
}
|
||||
|
||||
struct ScrapingModule: Codable, Identifiable, Hashable {
|
||||
let id: UUID
|
||||
let metadata: ModuleMetadata
|
||||
let localPath: String
|
||||
var isActive: Bool
|
||||
|
||||
init(id: UUID = UUID(), metadata: ModuleMetadata, localPath: String, isActive: Bool = false) {
|
||||
self.id = id
|
||||
self.metadata = metadata
|
||||
self.localPath = localPath
|
||||
self.isActive = isActive
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
|
||||
static func == (lhs: ScrapingModule, rhs: ScrapingModule) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
class ModuleManager: ObservableObject {
|
||||
@Published var modules: [ScrapingModule] = []
|
||||
private let fileManager = FileManager.default
|
||||
private let modulesFileName = "modules.json"
|
||||
|
||||
init() {
|
||||
loadModules()
|
||||
}
|
||||
|
||||
private func getDocumentsDirectory() -> URL {
|
||||
fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
}
|
||||
|
||||
private func getModulesFilePath() -> URL {
|
||||
getDocumentsDirectory().appendingPathComponent(modulesFileName)
|
||||
}
|
||||
|
||||
func loadModules() {
|
||||
let url = getModulesFilePath()
|
||||
guard let data = try? Data(contentsOf: url) else { return }
|
||||
modules = (try? JSONDecoder().decode([ScrapingModule].self, from: data)) ?? []
|
||||
}
|
||||
|
||||
private func saveModules() {
|
||||
let url = getModulesFilePath()
|
||||
guard let data = try? JSONEncoder().encode(modules) else { return }
|
||||
try? data.write(to: url)
|
||||
}
|
||||
|
||||
func addModule(metadataUrl: String) async throws -> ScrapingModule {
|
||||
guard let url = URL(string: metadataUrl) else {
|
||||
throw NSError(domain: "Invalid metadata URL", code: -1)
|
||||
}
|
||||
|
||||
let (metadataData, _) = try await URLSession.shared.data(from: url)
|
||||
let metadata = try JSONDecoder().decode(ModuleMetadata.self, from: metadataData)
|
||||
|
||||
guard let scriptUrl = URL(string: metadata.scriptUrl) else {
|
||||
throw NSError(domain: "Invalid script URL", code: -1)
|
||||
}
|
||||
|
||||
let (scriptData, _) = try await URLSession.shared.data(from: scriptUrl)
|
||||
guard let jsContent = String(data: scriptData, encoding: .utf8) else {
|
||||
throw NSError(domain: "Invalid script encoding", code: -1)
|
||||
}
|
||||
|
||||
let fileName = "\(UUID().uuidString).js"
|
||||
let localUrl = getDocumentsDirectory().appendingPathComponent(fileName)
|
||||
try jsContent.write(to: localUrl, atomically: true, encoding: .utf8)
|
||||
|
||||
let module = ScrapingModule(
|
||||
metadata: metadata,
|
||||
localPath: fileName
|
||||
)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.modules.append(module)
|
||||
self.saveModules()
|
||||
}
|
||||
|
||||
return module
|
||||
}
|
||||
|
||||
func deleteModule(_ module: ScrapingModule) {
|
||||
let localUrl = getDocumentsDirectory().appendingPathComponent(module.localPath)
|
||||
try? fileManager.removeItem(at: localUrl)
|
||||
|
||||
modules.removeAll { $0.id == module.id }
|
||||
saveModules()
|
||||
}
|
||||
|
||||
func getModuleContent(_ module: ScrapingModule) throws -> String {
|
||||
let localUrl = getDocumentsDirectory().appendingPathComponent(module.localPath)
|
||||
return try String(contentsOf: localUrl, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
133
Sora-JS/Settings.swift
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
//
|
||||
// Settings.swift
|
||||
// Sora-JS
|
||||
//
|
||||
// Created by Francesco on 05/01/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var moduleManager: ModuleManager
|
||||
@AppStorage("selectedModuleId") private var selectedModuleId: String?
|
||||
@State private var showingAddModule = false
|
||||
@State private var newModuleUrl = ""
|
||||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(moduleManager.modules) { module in
|
||||
ModuleRow(module: module, isSelected: module.id.uuidString == selectedModuleId)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
selectedModuleId = module.id.uuidString
|
||||
}
|
||||
.swipeActions {
|
||||
Button(role: .destructive) {
|
||||
if selectedModuleId == module.id.uuidString {
|
||||
selectedModuleId = nil
|
||||
}
|
||||
moduleManager.deleteModule(module)
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Scraping Modules")
|
||||
.toolbar {
|
||||
Button {
|
||||
showingAddModule = true
|
||||
} label: {
|
||||
Label("Add Module", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingAddModule) {
|
||||
NavigationView {
|
||||
Form {
|
||||
Section(header: Text("New Module")) {
|
||||
TextField("Module JSON URL", text: $newModuleUrl)
|
||||
.autocapitalization(.none)
|
||||
.keyboardType(.URL)
|
||||
}
|
||||
|
||||
if let error = errorMessage {
|
||||
Section {
|
||||
Text(error)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Add Module")
|
||||
.navigationBarItems(
|
||||
leading: Button("Cancel") {
|
||||
showingAddModule = false
|
||||
},
|
||||
trailing: Button("Add") {
|
||||
addModule()
|
||||
}
|
||||
.disabled(newModuleUrl.isEmpty || isLoading)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addModule() {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
Task {
|
||||
do {
|
||||
_ = try await moduleManager.addModule(metadataUrl: newModuleUrl)
|
||||
DispatchQueue.main.async {
|
||||
isLoading = false
|
||||
showingAddModule = false
|
||||
newModuleUrl = ""
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
isLoading = false
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ModuleRow: View {
|
||||
let module: ScrapingModule
|
||||
let isSelected: Bool
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
AsyncImage(url: URL(string: module.metadata.iconUrl)) { image in
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
} placeholder: {
|
||||
Color.gray
|
||||
}
|
||||
.frame(width: 40, height: 40)
|
||||
.cornerRadius(8)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(module.metadata.mediaType)
|
||||
.font(.headline)
|
||||
Text("by \(module.metadata.author)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
Text("\(module.metadata.language) • v\(module.metadata.version)")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Sora-JS/Sora_JSApp.swift
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// Sora_JSApp.swift
|
||||
// Sora-JS
|
||||
//
|
||||
// Created by Francesco on 04/01/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct Sora_JSApp: App {
|
||||
@StateObject private var moduleManager = ModuleManager()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.environmentObject(moduleManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,671 +0,0 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 55;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1308CFBC2D19844A004CD38C /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1308CFBB2D19844A004CD38C /* Double+Extension.swift */; };
|
||||
1308CFBE2D19844D004CD38C /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1308CFBD2D19844D004CD38C /* MusicProgressSlider.swift */; };
|
||||
1308CFC12D198466004CD38C /* CustomPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1308CFC02D198466004CD38C /* CustomPlayer.swift */; };
|
||||
132417842D13198000B4F2D2 /* SoraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417832D13198000B4F2D2 /* SoraApp.swift */; };
|
||||
132417862D13198000B4F2D2 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417852D13198000B4F2D2 /* ContentView.swift */; };
|
||||
132417882D13198200B4F2D2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 132417872D13198200B4F2D2 /* Assets.xcassets */; };
|
||||
1324178B2D13198200B4F2D2 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1324178A2D13198200B4F2D2 /* Preview Assets.xcassets */; };
|
||||
1324179E2D1319E800B4F2D2 /* MiruDataStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417932D1319E800B4F2D2 /* MiruDataStruct.swift */; };
|
||||
1324179F2D1319E800B4F2D2 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417952D1319E800B4F2D2 /* Notification.swift */; };
|
||||
132417A02D1319E800B4F2D2 /* HistoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417972D1319E800B4F2D2 /* HistoryManager.swift */; };
|
||||
132417A12D1319E800B4F2D2 /* ModuleStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417992D1319E800B4F2D2 /* ModuleStruct.swift */; };
|
||||
132417A22D1319E800B4F2D2 /* ModulesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1324179A2D1319E800B4F2D2 /* ModulesManager.swift */; };
|
||||
132417A32D1319E800B4F2D2 /* NormalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1324179C2D1319E800B4F2D2 /* NormalPlayer.swift */; };
|
||||
132417B82D131A0600B4F2D2 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417A72D131A0600B4F2D2 /* SearchView.swift */; };
|
||||
132417B92D131A0600B4F2D2 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417A82D131A0600B4F2D2 /* SearchResultsView.swift */; };
|
||||
132417BA2D131A0600B4F2D2 /* SettingsAboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417AB2D131A0600B4F2D2 /* SettingsAboutView.swift */; };
|
||||
132417BB2D131A0600B4F2D2 /* SettingsIUView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417AC2D131A0600B4F2D2 /* SettingsIUView.swift */; };
|
||||
132417BC2D131A0600B4F2D2 /* SettingsLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417AD2D131A0600B4F2D2 /* SettingsLogsView.swift */; };
|
||||
132417BD2D131A0600B4F2D2 /* SettingsModuleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417AE2D131A0600B4F2D2 /* SettingsModuleView.swift */; };
|
||||
132417BE2D131A0600B4F2D2 /* SettingsPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417AF2D131A0600B4F2D2 /* SettingsPlayerView.swift */; };
|
||||
132417BF2D131A0600B4F2D2 /* SettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417B02D131A0600B4F2D2 /* SettingView.swift */; };
|
||||
132417C02D131A0600B4F2D2 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417B12D131A0600B4F2D2 /* HomeView.swift */; };
|
||||
132417C12D131A0600B4F2D2 /* LibraryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417B32D131A0600B4F2D2 /* LibraryManager.swift */; };
|
||||
132417C22D131A0600B4F2D2 /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417B42D131A0600B4F2D2 /* LibraryView.swift */; };
|
||||
132417C32D131A0600B4F2D2 /* MediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417B62D131A0600B4F2D2 /* MediaView.swift */; };
|
||||
132417C42D131A0600B4F2D2 /* MediaExtraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417B72D131A0600B4F2D2 /* MediaExtraction.swift */; };
|
||||
132417CF2D131B7400B4F2D2 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 132417CE2D131B7400B4F2D2 /* SwiftSoup */; };
|
||||
132417D22D131C5300B4F2D2 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 132417D12D131C5300B4F2D2 /* Kingfisher */; };
|
||||
132417D52D13240200B4F2D2 /* EpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417D42D13240200B4F2D2 /* EpisodeCell.swift */; };
|
||||
132417D72D13242400B4F2D2 /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417D62D13242400B4F2D2 /* CircularProgressBar.swift */; };
|
||||
132417D92D1328B900B4F2D2 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417D82D1328B900B4F2D2 /* VideoPlayerView.swift */; };
|
||||
1352BA712D1ABC30000A9AF9 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1352BA702D1ABC30000A9AF9 /* URLSession.swift */; };
|
||||
13AEE7BA2D2451F200CA634A /* GitHubAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13AEE7B92D2451F200CA634A /* GitHubAPI.swift */; };
|
||||
13AEE7BC2D24521200CA634A /* SettingsReleasesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13AEE7BB2D24521200CA634A /* SettingsReleasesView.swift */; };
|
||||
13B3A4B22D1477F100BCC0D5 /* SettingsStorageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13B3A4B12D1477F100BCC0D5 /* SettingsStorageView.swift */; };
|
||||
13B544B02D26D8E900CC6C59 /* OpenCastSwift iOS in Frameworks */ = {isa = PBXBuildFile; productRef = 13B544AF2D26D8E900CC6C59 /* OpenCastSwift iOS */; };
|
||||
13C9821F2D2152B1007A0132 /* GitHubRelease.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13C9821E2D2152B1007A0132 /* GitHubRelease.swift */; };
|
||||
13ED65752D284045008F4C23 /* SettingsEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13ED65742D284045008F4C23 /* SettingsEditorView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1308CFBB2D19844A004CD38C /* Double+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; };
|
||||
1308CFBD2D19844D004CD38C /* MusicProgressSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = "<group>"; };
|
||||
1308CFC02D198466004CD38C /* CustomPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPlayer.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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 /* MediaView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = "<group>"; };
|
||||
132417B72D131A0600B4F2D2 /* MediaExtraction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaExtraction.swift; sourceTree = "<group>"; };
|
||||
132417C52D131AA500B4F2D2 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
132417D42D13240200B4F2D2 /* EpisodeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeCell.swift; sourceTree = "<group>"; };
|
||||
132417D62D13242400B4F2D2 /* CircularProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = "<group>"; };
|
||||
132417D82D1328B900B4F2D2 /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = "<group>"; };
|
||||
1352BA6F2D1AB113000A9AF9 /* Sora.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Sora.entitlements; sourceTree = "<group>"; };
|
||||
1352BA702D1ABC30000A9AF9 /* URLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSession.swift; sourceTree = "<group>"; };
|
||||
13AEE7B92D2451F200CA634A /* GitHubAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubAPI.swift; sourceTree = "<group>"; };
|
||||
13AEE7BB2D24521200CA634A /* SettingsReleasesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsReleasesView.swift; sourceTree = "<group>"; };
|
||||
13B3A4B12D1477F100BCC0D5 /* SettingsStorageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStorageView.swift; sourceTree = "<group>"; };
|
||||
13C9821E2D2152B1007A0132 /* GitHubRelease.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubRelease.swift; sourceTree = "<group>"; };
|
||||
13ED65742D284045008F4C23 /* SettingsEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsEditorView.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
1324177D2D13198000B4F2D2 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
132417D22D131C5300B4F2D2 /* Kingfisher in Frameworks */,
|
||||
132417CF2D131B7400B4F2D2 /* SwiftSoup in Frameworks */,
|
||||
13B544B02D26D8E900CC6C59 /* OpenCastSwift iOS in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
1308CFBA2D19843E004CD38C /* CustomPlayer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1308CFC02D198466004CD38C /* CustomPlayer.swift */,
|
||||
1308CFBF2D198450004CD38C /* Components */,
|
||||
);
|
||||
path = CustomPlayer;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1308CFBF2D198450004CD38C /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1308CFBD2D19844D004CD38C /* MusicProgressSlider.swift */,
|
||||
1308CFBB2D19844A004CD38C /* Double+Extension.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = (
|
||||
1352BA6F2D1AB113000A9AF9 /* Sora.entitlements */,
|
||||
132417C52D131AA500B4F2D2 /* Info.plist */,
|
||||
132417912D1319E800B4F2D2 /* Utils */,
|
||||
132417A52D131A0600B4F2D2 /* Views */,
|
||||
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 = (
|
||||
13C9821D2D2152A0007A0132 /* GitHub */,
|
||||
1308CFBA2D19843E004CD38C /* CustomPlayer */,
|
||||
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 */,
|
||||
1352BA702D1ABC30000A9AF9 /* URLSession.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 = (
|
||||
132417D82D1328B900B4F2D2 /* VideoPlayerView.swift */,
|
||||
1324179C2D1319E800B4F2D2 /* NormalPlayer.swift */,
|
||||
);
|
||||
path = Player;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417A52D131A0600B4F2D2 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
132417B12D131A0600B4F2D2 /* HomeView.swift */,
|
||||
132417B22D131A0600B4F2D2 /* LibraryViews */,
|
||||
132417A62D131A0600B4F2D2 /* SearchViews */,
|
||||
132417A92D131A0600B4F2D2 /* SettingsViews */,
|
||||
132417B52D131A0600B4F2D2 /* MediaViews */,
|
||||
);
|
||||
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 */,
|
||||
13B3A4B12D1477F100BCC0D5 /* SettingsStorageView.swift */,
|
||||
13AEE7BB2D24521200CA634A /* SettingsReleasesView.swift */,
|
||||
13ED65742D284045008F4C23 /* SettingsEditorView.swift */,
|
||||
);
|
||||
path = SubPages;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417B22D131A0600B4F2D2 /* LibraryViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
132417B42D131A0600B4F2D2 /* LibraryView.swift */,
|
||||
132417B32D131A0600B4F2D2 /* LibraryManager.swift */,
|
||||
);
|
||||
path = LibraryViews;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417B52D131A0600B4F2D2 /* MediaViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
132417D32D1323F500B4F2D2 /* EpisodesCell */,
|
||||
132417B62D131A0600B4F2D2 /* MediaView.swift */,
|
||||
132417B72D131A0600B4F2D2 /* MediaExtraction.swift */,
|
||||
);
|
||||
path = MediaViews;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
132417D32D1323F500B4F2D2 /* EpisodesCell */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
132417D42D13240200B4F2D2 /* EpisodeCell.swift */,
|
||||
132417D62D13242400B4F2D2 /* CircularProgressBar.swift */,
|
||||
);
|
||||
path = EpisodesCell;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
13C9821D2D2152A0007A0132 /* GitHub */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13C9821E2D2152B1007A0132 /* GitHubRelease.swift */,
|
||||
13AEE7B92D2451F200CA634A /* GitHubAPI.swift */,
|
||||
);
|
||||
path = GitHub;
|
||||
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 = (
|
||||
132417CE2D131B7400B4F2D2 /* SwiftSoup */,
|
||||
132417D12D131C5300B4F2D2 /* Kingfisher */,
|
||||
13B544AF2D26D8E900CC6C59 /* OpenCastSwift iOS */,
|
||||
);
|
||||
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 = (
|
||||
132417CD2D131B7400B4F2D2 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
|
||||
132417D02D131C5300B4F2D2 /* XCRemoteSwiftPackageReference "Kingfisher" */,
|
||||
13B544AE2D26D8E900CC6C59 /* XCRemoteSwiftPackageReference "OpenCastSwift" */,
|
||||
);
|
||||
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 = (
|
||||
13B3A4B22D1477F100BCC0D5 /* SettingsStorageView.swift in Sources */,
|
||||
132417BB2D131A0600B4F2D2 /* SettingsIUView.swift in Sources */,
|
||||
132417C42D131A0600B4F2D2 /* MediaExtraction.swift in Sources */,
|
||||
132417B82D131A0600B4F2D2 /* SearchView.swift in Sources */,
|
||||
1308CFBC2D19844A004CD38C /* Double+Extension.swift in Sources */,
|
||||
132417D92D1328B900B4F2D2 /* VideoPlayerView.swift in Sources */,
|
||||
1324179F2D1319E800B4F2D2 /* Notification.swift in Sources */,
|
||||
132417BD2D131A0600B4F2D2 /* SettingsModuleView.swift in Sources */,
|
||||
132417BC2D131A0600B4F2D2 /* SettingsLogsView.swift in Sources */,
|
||||
1308CFC12D198466004CD38C /* CustomPlayer.swift in Sources */,
|
||||
132417A22D1319E800B4F2D2 /* ModulesManager.swift in Sources */,
|
||||
132417862D13198000B4F2D2 /* ContentView.swift in Sources */,
|
||||
13AEE7BA2D2451F200CA634A /* GitHubAPI.swift in Sources */,
|
||||
132417C22D131A0600B4F2D2 /* LibraryView.swift in Sources */,
|
||||
132417A32D1319E800B4F2D2 /* NormalPlayer.swift in Sources */,
|
||||
132417D72D13242400B4F2D2 /* CircularProgressBar.swift in Sources */,
|
||||
132417C02D131A0600B4F2D2 /* HomeView.swift in Sources */,
|
||||
132417BF2D131A0600B4F2D2 /* SettingView.swift in Sources */,
|
||||
132417C32D131A0600B4F2D2 /* MediaView.swift in Sources */,
|
||||
132417A12D1319E800B4F2D2 /* ModuleStruct.swift in Sources */,
|
||||
132417B92D131A0600B4F2D2 /* SearchResultsView.swift in Sources */,
|
||||
13AEE7BC2D24521200CA634A /* SettingsReleasesView.swift in Sources */,
|
||||
132417842D13198000B4F2D2 /* SoraApp.swift in Sources */,
|
||||
132417BE2D131A0600B4F2D2 /* SettingsPlayerView.swift in Sources */,
|
||||
132417C12D131A0600B4F2D2 /* LibraryManager.swift in Sources */,
|
||||
132417BA2D131A0600B4F2D2 /* SettingsAboutView.swift in Sources */,
|
||||
1324179E2D1319E800B4F2D2 /* MiruDataStruct.swift in Sources */,
|
||||
13ED65752D284045008F4C23 /* SettingsEditorView.swift in Sources */,
|
||||
1308CFBE2D19844D004CD38C /* MusicProgressSlider.swift in Sources */,
|
||||
132417D52D13240200B4F2D2 /* EpisodeCell.swift in Sources */,
|
||||
13C9821F2D2152B1007A0132 /* GitHubRelease.swift in Sources */,
|
||||
1352BA712D1ABC30000A9AF9 /* URLSession.swift in Sources */,
|
||||
132417A02D1319E800B4F2D2 /* HistoryManager.swift in Sources */,
|
||||
);
|
||||
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_ENTITLEMENTS = Sora/Sora.entitlements;
|
||||
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_FILE = Sora/Info.plist;
|
||||
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 = 0.1.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.cranci.Sora;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
132417902D13198200B4F2D2 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = Sora/Sora.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 399LMK6Q2Y;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Sora/Info.plist;
|
||||
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 = 0.1.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.cranci.Sora;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = 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 */
|
||||
132417CD2D131B7400B4F2D2 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/scinfu/SwiftSoup.git";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 2.4.0;
|
||||
};
|
||||
};
|
||||
132417D02D131C5300B4F2D2 /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/onevcat/Kingfisher.git";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 7.9.1;
|
||||
};
|
||||
};
|
||||
13B544AE2D26D8E900CC6C59 /* XCRemoteSwiftPackageReference "OpenCastSwift" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/danwilliams64/OpenCastSwift.git";
|
||||
requirement = {
|
||||
branch = master;
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
132417CE2D131B7400B4F2D2 /* SwiftSoup */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 132417CD2D131B7400B4F2D2 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
|
||||
productName = SwiftSoup;
|
||||
};
|
||||
132417D12D131C5300B4F2D2 /* Kingfisher */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 132417D02D131C5300B4F2D2 /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
||||
productName = Kingfisher;
|
||||
};
|
||||
13B544AF2D26D8E900CC6C59 /* OpenCastSwift iOS */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 13B544AE2D26D8E900CC6C59 /* XCRemoteSwiftPackageReference "OpenCastSwift" */;
|
||||
productName = "OpenCastSwift iOS";
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 132417782D13198000B4F2D2 /* Project object */;
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "Kingfisher",
|
||||
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "b6f62758f21a8c03cd64f4009c037cfa580a256e",
|
||||
"version": "7.9.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "OpenCastSwift",
|
||||
"repositoryURL": "https://github.com/danwilliams64/OpenCastSwift.git",
|
||||
"state": {
|
||||
"branch": "master",
|
||||
"revision": "f96bf1ed9c1dcad34a1fcccb50d79a6cf612def4",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SwiftProtobuf",
|
||||
"repositoryURL": "https://github.com/apple/swift-protobuf",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "9f0c76544701845ad98716f3f6a774a892152bcb",
|
||||
"version": "1.26.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SwiftSoup",
|
||||
"repositoryURL": "https://github.com/scinfu/SwiftSoup.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "5386dab25134eec11fc35fc5e43caf422fad0270",
|
||||
"version": "2.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SwiftyJSON",
|
||||
"repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "af76cf3ef710b6ca5f8c05f3a31307d44a3c5828",
|
||||
"version": "5.0.2"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
<?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>Playground (Playground) 1.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>Playground (Playground) 2.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>3</integer>
|
||||
</dict>
|
||||
<key>Playground (Playground).xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</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
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 384 B |
|
Before Width: | Height: | Size: 565 B |
|
Before Width: | Height: | Size: 832 B |
|
Before Width: | Height: | Size: 832 B |
|
Before Width: | Height: | Size: 832 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
|
@ -1,118 +0,0 @@
|
|||
//
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
checkForUpdate()
|
||||
Logger.shared.log("Started Sora")
|
||||
}
|
||||
}
|
||||
|
||||
func checkForUpdate() {
|
||||
fetchLatestRelease { release in
|
||||
guard let release = release else { return }
|
||||
|
||||
let latestVersion = release.tagName
|
||||
let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.1.1"
|
||||
|
||||
if latestVersion.compare(currentVersion, options: .numeric) == .orderedDescending {
|
||||
DispatchQueue.main.async {
|
||||
showUpdateAlert(release: release)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchLatestRelease(completion: @escaping (GitHubRelease?) -> Void) {
|
||||
let url = URL(string: "https://api.github.com/repos/cranci1/Sora/releases/latest")!
|
||||
|
||||
URLSession.custom.dataTask(with: url) { data, _, error in
|
||||
guard let data = data, error == nil else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let release = try? JSONDecoder().decode(GitHubRelease.self, from: data)
|
||||
completion(release)
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func showUpdateAlert(release: GitHubRelease) {
|
||||
let alert = UIAlertController(title: "Update Available", message: "A new version (\(release.tagName)) is available. Would you like to update Sora?", preferredStyle: .alert)
|
||||
|
||||
alert.addAction(UIAlertAction(title: "Update", style: .default, handler: { _ in
|
||||
self.showInstallOptionsAlert(release: release)
|
||||
}))
|
||||
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootViewController = windowScene.windows.first?.rootViewController {
|
||||
rootViewController.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func showInstallOptionsAlert(release: GitHubRelease) {
|
||||
let installAlert = UIAlertController(title: "Install Update", message: "Choose an installation method:", preferredStyle: .alert)
|
||||
|
||||
let downloadUrl = release.assets.first?.browserDownloadUrl ?? ""
|
||||
|
||||
installAlert.addAction(UIAlertAction(title: "Install in AltStore", style: .default, handler: { _ in
|
||||
if let url = URL(string: "altstore://install?url=\(downloadUrl)") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}))
|
||||
|
||||
installAlert.addAction(UIAlertAction(title: "Install in Sidestore", style: .default, handler: { _ in
|
||||
if let url = URL(string: "sidestore://install?url=\(downloadUrl)") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}))
|
||||
|
||||
installAlert.addAction(UIAlertAction(title: "Open in Safari", style: .default, handler: { _ in
|
||||
if let url = URL(string: downloadUrl) {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}))
|
||||
|
||||
installAlert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootViewController = windowScene.windows.first?.rootViewController {
|
||||
rootViewController.present(installAlert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<?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>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
//
|
||||
// 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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
//
|
||||
// Double+Extension.swift
|
||||
// AppleMusicSlider
|
||||
//
|
||||
// Created by Pratik on 14/01/23.
|
||||
//
|
||||
// Thanks to pratikg29 for this code inside his open source project "https://github.com/pratikg29/Custom-Slider-Control?ref=iosexample.com"
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Double {
|
||||
func asTimeString(style: DateComponentsFormatter.UnitsStyle) -> String {
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.allowedUnits = [.minute, .second]
|
||||
formatter.unitsStyle = style
|
||||
formatter.zeroFormattingBehavior = .pad
|
||||
return formatter.string(from: self) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
extension BinaryFloatingPoint {
|
||||
func asTimeString(style: DateComponentsFormatter.UnitsStyle) -> String {
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.allowedUnits = [.minute, .second]
|
||||
formatter.unitsStyle = style
|
||||
formatter.zeroFormattingBehavior = .pad
|
||||
return formatter.string(from: TimeInterval(self)) ?? ""
|
||||
}
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
//
|
||||
// MusicProgressSlider.swift
|
||||
// Custom Seekbar
|
||||
//
|
||||
// Created by Pratik on 08/01/23.
|
||||
//
|
||||
// Thanks to pratikg29 for this code inside his open source project "https://github.com/pratikg29/Custom-Slider-Control?ref=iosexample.com"
|
||||
// I did edit just a little bit the code for my liking
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
|
||||
@Binding var value: T
|
||||
let inRange: ClosedRange<T>
|
||||
let activeFillColor: Color
|
||||
let fillColor: Color
|
||||
let emptyColor: Color
|
||||
let height: CGFloat
|
||||
let onEditingChanged: (Bool) -> Void
|
||||
|
||||
@State private var localRealProgress: T = 0
|
||||
@State private var localTempProgress: T = 0
|
||||
@GestureState private var isActive: Bool = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { bounds in
|
||||
ZStack {
|
||||
VStack {
|
||||
ZStack(alignment: .center) {
|
||||
Capsule()
|
||||
.fill(emptyColor)
|
||||
Capsule()
|
||||
.fill(isActive ? activeFillColor : fillColor)
|
||||
.mask({
|
||||
HStack {
|
||||
Rectangle()
|
||||
.frame(width: max(bounds.size.width * CGFloat((localRealProgress + localTempProgress)), 0), alignment: .leading)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text(value.asTimeString(style: .positional))
|
||||
Spacer(minLength: 0)
|
||||
Text("-" + (inRange.upperBound - value).asTimeString(style: .positional))
|
||||
}
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(isActive ? fillColor : emptyColor)
|
||||
}
|
||||
.frame(width: isActive ? bounds.size.width * 1.04 : bounds.size.width, alignment: .center)
|
||||
.animation(animation, value: isActive)
|
||||
}
|
||||
.frame(width: bounds.size.width, height: bounds.size.height, alignment: .center)
|
||||
.contentShape(Rectangle())
|
||||
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
|
||||
.updating($isActive) { _, state, _ in
|
||||
state = true
|
||||
}
|
||||
.onChanged { gesture in
|
||||
localTempProgress = T(gesture.translation.width / bounds.size.width)
|
||||
value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound)
|
||||
}.onEnded { _ in
|
||||
localRealProgress = max(min(localRealProgress + localTempProgress, 1), 0)
|
||||
localTempProgress = 0
|
||||
})
|
||||
.onChange(of: isActive) { newValue in
|
||||
value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound)
|
||||
onEditingChanged(newValue)
|
||||
}
|
||||
.onAppear {
|
||||
localRealProgress = getPrgPercentage(value)
|
||||
}
|
||||
.onChange(of: value) { newValue in
|
||||
if !isActive {
|
||||
localRealProgress = getPrgPercentage(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: isActive ? height * 1.25 : height, alignment: .center)
|
||||
}
|
||||
|
||||
private var animation: Animation {
|
||||
if isActive {
|
||||
return .spring()
|
||||
} else {
|
||||
return .spring(response: 0.5, dampingFraction: 0.5, blendDuration: 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
private func getPrgPercentage(_ value: T) -> T {
|
||||
let range = inRange.upperBound - inRange.lowerBound
|
||||
let correctedStartValue = value - inRange.lowerBound
|
||||
let percentage = correctedStartValue / range
|
||||
return percentage
|
||||
}
|
||||
|
||||
private func getPrgValue() -> T {
|
||||
return ((localRealProgress + localTempProgress) * (inRange.upperBound - inRange.lowerBound)) + inRange.lowerBound
|
||||
}
|
||||
}
|
||||
|
|
@ -1,271 +0,0 @@
|
|||
//
|
||||
// ContentView.swift
|
||||
// test2
|
||||
//
|
||||
// Created by Francesco on 20/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
|
||||
struct CustomVideoPlayer: UIViewControllerRepresentable {
|
||||
let player: AVPlayer
|
||||
|
||||
func makeUIViewController(context: Context) -> AVPlayerViewController {
|
||||
let controller = NormalPlayer()
|
||||
controller.player = player
|
||||
controller.showsPlaybackControls = false
|
||||
player.play()
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
|
||||
// yes? Like the plural of the famous american rapper ye? -IBHRAD
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomMediaPlayer: View {
|
||||
@State private var player: AVPlayer
|
||||
@State private var isPlaying = true
|
||||
@State private var currentTime: Double = 0.0
|
||||
@State private var duration: Double = 0.0
|
||||
@State private var showControls = false
|
||||
@State private var inactivityTimer: Timer?
|
||||
@State private var timeObserverToken: Any?
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
let module: ModuleStruct
|
||||
let fullUrl: String
|
||||
let title: String
|
||||
let episodeNumber: Int
|
||||
let onWatchNext: () -> Void
|
||||
|
||||
init(module: ModuleStruct, urlString: String, fullUrl: String, title: String, episodeNumber: Int, onWatchNext: @escaping () -> Void) {
|
||||
guard let url = URL(string: urlString) else {
|
||||
fatalError("Invalid URL string")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
if urlString.contains("ascdn") {
|
||||
request.addValue("\(module.module[0].details.baseURL)", forHTTPHeaderField: "Referer")
|
||||
}
|
||||
|
||||
let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]])
|
||||
_player = State(initialValue: AVPlayer(playerItem: AVPlayerItem(asset: asset)))
|
||||
|
||||
self.module = module
|
||||
self.fullUrl = fullUrl
|
||||
self.title = title
|
||||
self.episodeNumber = episodeNumber
|
||||
self.onWatchNext = onWatchNext
|
||||
|
||||
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(fullUrl)")
|
||||
if lastPlayedTime > 0 {
|
||||
let seekTime = CMTime(seconds: lastPlayedTime, preferredTimescale: 1)
|
||||
self._player.wrappedValue.seek(to: seekTime)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
VStack {
|
||||
ZStack {
|
||||
CustomVideoPlayer(player: player)
|
||||
.onAppear {
|
||||
player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 600), queue: .main) { time in
|
||||
currentTime = time.seconds
|
||||
if let itemDuration = player.currentItem?.duration.seconds, itemDuration.isFinite && !itemDuration.isNaN {
|
||||
duration = itemDuration
|
||||
}
|
||||
}
|
||||
startUpdatingCurrentTime()
|
||||
addPeriodicTimeObserver(fullURL: fullUrl)
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.overlay(
|
||||
Group {
|
||||
if showControls {
|
||||
Color.black.opacity(0.5)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
HStack(spacing: 20) {
|
||||
Button(action: {
|
||||
currentTime = max(currentTime - 10, 0)
|
||||
player.seek(to: CMTime(seconds: currentTime, preferredTimescale: 600))
|
||||
}) {
|
||||
Image(systemName: "gobackward.10")
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 25))
|
||||
.contentShape(Rectangle())
|
||||
.frame(width: 60, height: 60)
|
||||
|
||||
Button(action: {
|
||||
if isPlaying {
|
||||
player.pause()
|
||||
} else {
|
||||
player.play()
|
||||
}
|
||||
isPlaying.toggle()
|
||||
}) {
|
||||
Image(systemName: isPlaying ? "pause.fill" : "play.fill")
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 45))
|
||||
.contentShape(Rectangle())
|
||||
.frame(width: 80, height: 80)
|
||||
|
||||
Button(action: {
|
||||
currentTime = min(currentTime + 10, duration)
|
||||
player.seek(to: CMTime(seconds: currentTime, preferredTimescale: 600))
|
||||
}) {
|
||||
Image(systemName: "goforward.10")
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 25))
|
||||
.contentShape(Rectangle())
|
||||
.frame(width: 60, height: 60)
|
||||
}
|
||||
}
|
||||
}
|
||||
.animation(.easeInOut(duration: 0.2), value: showControls),
|
||||
alignment: .center
|
||||
)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
showControls.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
HStack(alignment: .bottom) {
|
||||
if showControls {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Episode \(episodeNumber)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
Spacer()
|
||||
if duration - currentTime <= duration * 0.10 && currentTime != duration {
|
||||
Button(action: {
|
||||
player.pause()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
onWatchNext()
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "forward.fill")
|
||||
.foregroundColor(Color.black)
|
||||
Text("Watch Next")
|
||||
.font(.headline)
|
||||
.foregroundColor(Color.black)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.cornerRadius(32)
|
||||
}
|
||||
.padding(.trailing, 10)
|
||||
}
|
||||
if showControls {
|
||||
Menu {
|
||||
Menu("Playback Speed") {
|
||||
ForEach([0.5, 1.0, 1.25, 1.5, 1.75, 2.0], id: \.self) { speed in
|
||||
Button(action: {
|
||||
player.rate = Float(speed)
|
||||
if player.timeControlStatus != .playing {
|
||||
player.pause()
|
||||
}
|
||||
}) {
|
||||
Text("\(speed, specifier: "%.2f")")
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 15))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.trailing, 32)
|
||||
|
||||
if showControls {
|
||||
MusicProgressSlider(
|
||||
value: $currentTime,
|
||||
inRange: 0...duration,
|
||||
activeFillColor: .white,
|
||||
fillColor: .white.opacity(0.5),
|
||||
emptyColor: .white.opacity(0.3),
|
||||
height: 28,
|
||||
onEditingChanged: { editing in
|
||||
if !editing {
|
||||
player.seek(to: CMTime(seconds: currentTime, preferredTimescale: 600))
|
||||
}
|
||||
}
|
||||
)
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.bottom, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
startUpdatingCurrentTime()
|
||||
}
|
||||
.onDisappear {
|
||||
player.pause()
|
||||
inactivityTimer?.invalidate()
|
||||
if let timeObserverToken = timeObserverToken {
|
||||
player.removeTimeObserver(timeObserverToken)
|
||||
self.timeObserverToken = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
VStack {
|
||||
if showControls {
|
||||
HStack {
|
||||
Button(action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}) {
|
||||
Image(systemName: "xmark")
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 20))
|
||||
}
|
||||
.frame(width: 60, height: 60)
|
||||
.contentShape(Rectangle())
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startUpdatingCurrentTime() {
|
||||
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
||||
currentTime = player.currentTime().seconds
|
||||
}
|
||||
}
|
||||
|
||||
private func addPeriodicTimeObserver(fullURL: String) {
|
||||
let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||
timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in
|
||||
guard let currentItem = player.currentItem,
|
||||
currentItem.duration.seconds.isFinite else {
|
||||
return
|
||||
}
|
||||
|
||||
let currentTime = time.seconds
|
||||
let duration = currentItem.duration.seconds
|
||||
|
||||
UserDefaults.standard.set(currentTime, forKey: "lastPlayedTime_\(fullURL)")
|
||||
UserDefaults.standard.set(duration, forKey: "totalTime_\(fullURL)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
//
|
||||
// Notification.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Notification.Name {
|
||||
static let moduleAdded = Notification.Name("moduleAdded")
|
||||
static let moduleRemoved = Notification.Name("moduleRemoved")
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
//
|
||||
// URLSession.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 24/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension URLSession {
|
||||
static let custom: URLSession = {
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.httpAdditionalHeaders = [
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
||||
]
|
||||
return URLSession(configuration: configuration)
|
||||
}()
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
//
|
||||
// GitHubAPI.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 31/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GitHubReleases: Codable {
|
||||
let tagName: String
|
||||
let body: String
|
||||
let htmlUrl: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case tagName = "tag_name"
|
||||
case body
|
||||
case htmlUrl = "html_url"
|
||||
}
|
||||
}
|
||||
|
||||
class GitHubAPI {
|
||||
static let shared = GitHubAPI()
|
||||
|
||||
func fetchReleases(completion: @escaping ([GitHubReleases]?) -> Void) {
|
||||
let url = URL(string: "https://api.github.com/repos/cranci1/Sora/releases")!
|
||||
|
||||
URLSession.custom.dataTask(with: url) { data, _, error in
|
||||
guard let data = data, error == nil else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let releases = try? JSONDecoder().decode([GitHubReleases].self, from: data)
|
||||
completion(releases)
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
//
|
||||
// GitHubRelease.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 29/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GitHubRelease: Codable {
|
||||
let url: String
|
||||
let assetsUrl: String
|
||||
let uploadUrl: String
|
||||
let htmlUrl: String
|
||||
let id: Int
|
||||
let author: Author
|
||||
let nodeId: String
|
||||
let tagName: String
|
||||
let targetCommitish: String
|
||||
let name: String
|
||||
let draft: Bool
|
||||
let prerelease: Bool
|
||||
let createdAt: String
|
||||
let publishedAt: String
|
||||
let assets: [Asset]
|
||||
let tarballUrl: String
|
||||
let zipballUrl: String
|
||||
let body: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case url
|
||||
case assetsUrl = "assets_url"
|
||||
case uploadUrl = "upload_url"
|
||||
case htmlUrl = "html_url"
|
||||
case id
|
||||
case author
|
||||
case nodeId = "node_id"
|
||||
case tagName = "tag_name"
|
||||
case targetCommitish = "target_commitish"
|
||||
case name
|
||||
case draft
|
||||
case prerelease
|
||||
case createdAt = "created_at"
|
||||
case publishedAt = "published_at"
|
||||
case assets
|
||||
case tarballUrl = "tarball_url"
|
||||
case zipballUrl = "zipball_url"
|
||||
case body
|
||||
}
|
||||
|
||||
struct Author: Codable {
|
||||
let login: String
|
||||
let id: Int
|
||||
let nodeId: String
|
||||
let avatarUrl: String
|
||||
let gravatarId: String
|
||||
let url: String
|
||||
let htmlUrl: String
|
||||
let followersUrl: String
|
||||
let followingUrl: String
|
||||
let gistsUrl: String
|
||||
let starredUrl: String
|
||||
let subscriptionsUrl: String
|
||||
let organizationsUrl: String
|
||||
let reposUrl: String
|
||||
let eventsUrl: String
|
||||
let receivedEventsUrl: String
|
||||
let type: String
|
||||
let siteAdmin: Bool
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case login
|
||||
case id
|
||||
case nodeId = "node_id"
|
||||
case avatarUrl = "avatar_url"
|
||||
case gravatarId = "gravatar_id"
|
||||
case url
|
||||
case htmlUrl = "html_url"
|
||||
case followersUrl = "followers_url"
|
||||
case followingUrl = "following_url"
|
||||
case gistsUrl = "gists_url"
|
||||
case starredUrl = "starred_url"
|
||||
case subscriptionsUrl = "subscriptions_url"
|
||||
case organizationsUrl = "organizations_url"
|
||||
case reposUrl = "repos_url"
|
||||
case eventsUrl = "events_url"
|
||||
case receivedEventsUrl = "received_events_url"
|
||||
case type
|
||||
case siteAdmin = "site_admin"
|
||||
}
|
||||
}
|
||||
|
||||
struct Asset: Codable {
|
||||
let url: String
|
||||
let id: Int
|
||||
let nodeId: String
|
||||
let name: String
|
||||
let label: String?
|
||||
let uploader: Author
|
||||
let contentType: String
|
||||
let state: String
|
||||
let size: Int
|
||||
let downloadCount: Int
|
||||
let createdAt: String
|
||||
let updatedAt: String
|
||||
let browserDownloadUrl: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case url
|
||||
case id
|
||||
case nodeId = "node_id"
|
||||
case name
|
||||
case label
|
||||
case uploader
|
||||
case contentType = "content_type"
|
||||
case state
|
||||
case size
|
||||
case downloadCount = "download_count"
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
case browserDownloadUrl = "browser_download_url"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
//
|
||||
// HistoryManager.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
class HistoryManager: ObservableObject {
|
||||
@Published var searchHistory: [String] = UserDefaults.standard.stringArray(forKey: "SearchHistory") ?? []
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init() {
|
||||
NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)
|
||||
.sink { [weak self] _ in
|
||||
DispatchQueue.main.async {
|
||||
self?.searchHistory = UserDefaults.standard.stringArray(forKey: "SearchHistory") ?? []
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func addSearchHistory(_ item: String) {
|
||||
if !searchHistory.contains(item) {
|
||||
searchHistory.insert(item, at: 0)
|
||||
UserDefaults.standard.set(searchHistory, forKey: "SearchHistory")
|
||||
}
|
||||
}
|
||||
|
||||
func deleteHistoryItem(at offsets: IndexSet) {
|
||||
searchHistory.remove(atOffsets: offsets)
|
||||
UserDefaults.standard.set(searchHistory, forKey: "SearchHistory")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
//
|
||||
// MiruDataStruct.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct MiruDataStruct: Codable {
|
||||
var likes: [Like]
|
||||
|
||||
struct Like: Codable {
|
||||
let anilistID: Int
|
||||
var gogoSlug: String
|
||||
let title: String
|
||||
let cover: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case anilistID = "anilist_id"
|
||||
case gogoSlug = "gogo_slug"
|
||||
case title
|
||||
case cover
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
//
|
||||
// ModuleStruct.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ModuleStruct: Codable {
|
||||
let name: String
|
||||
let version: String
|
||||
let author: Author
|
||||
let iconURL: String
|
||||
let stream: String
|
||||
let language: String
|
||||
let extractor: String
|
||||
let module: [Module]
|
||||
|
||||
struct Author: Codable {
|
||||
let name: String
|
||||
let website: String
|
||||
}
|
||||
|
||||
struct Module: Codable, Hashable {
|
||||
let search: Search
|
||||
let featured: Featured
|
||||
let details: Details
|
||||
let episodes: Episodes
|
||||
|
||||
struct Search: Codable, Hashable {
|
||||
let url: String
|
||||
let parameter: String
|
||||
let documentSelector: String
|
||||
let title: String
|
||||
let image: Image
|
||||
let href: String
|
||||
let searchable: Bool?
|
||||
|
||||
struct Image: Codable, Hashable {
|
||||
let url: String
|
||||
let attribute: String
|
||||
}
|
||||
}
|
||||
|
||||
struct Featured: Codable, Hashable {
|
||||
let url: String
|
||||
let documentSelector: String
|
||||
let title: String
|
||||
let image: Image
|
||||
let href: String
|
||||
|
||||
struct Image: Codable, Hashable {
|
||||
let url: String
|
||||
let attribute: String
|
||||
}
|
||||
}
|
||||
|
||||
struct Details: Codable, Hashable {
|
||||
let baseURL: String
|
||||
let pageRedirects: Bool?
|
||||
let aliases: Aliases
|
||||
let synopsis: String
|
||||
let airdate: String
|
||||
let stars: String
|
||||
|
||||
struct Aliases: Codable, Hashable {
|
||||
let selector: String
|
||||
let attribute: String
|
||||
}
|
||||
}
|
||||
|
||||
struct Episodes: Codable, Hashable {
|
||||
let selector: String
|
||||
let order: String
|
||||
let pattern: String
|
||||
let pattern2: String?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
//
|
||||
// ModulesManager.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ModulesManager: ObservableObject {
|
||||
@Published var modules: [ModuleStruct] = []
|
||||
@Published var isLoading = true
|
||||
var moduleURLs: [String: String] = [:]
|
||||
private let modulesFileName = "modules.json"
|
||||
private let moduleURLsFileName = "moduleURLs.json"
|
||||
|
||||
init() {
|
||||
loadModules()
|
||||
}
|
||||
|
||||
func loadModules() {
|
||||
isLoading = true
|
||||
loadModuleURLs()
|
||||
loadModuleData()
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
func addModule(from urlString: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let url = URL(string: urlString) else {
|
||||
completion(.failure(ModuleError.invalidURL))
|
||||
return
|
||||
}
|
||||
let task = URLSession.custom.dataTask(with: url) { data, _, error in
|
||||
guard let data = data, error == nil else {
|
||||
completion(.failure(error ?? ModuleError.unknown))
|
||||
return
|
||||
}
|
||||
do {
|
||||
let module = try JSONDecoder().decode(ModuleStruct.self, from: data)
|
||||
DispatchQueue.main.async {
|
||||
if !self.modules.contains(where: { $0.name == module.name }) {
|
||||
self.modules.append(module)
|
||||
self.moduleURLs[module.name] = urlString
|
||||
self.saveModuleData()
|
||||
self.saveModuleURLs()
|
||||
NotificationCenter.default.post(name: .moduleAdded, object: nil)
|
||||
completion(.success(()))
|
||||
} else {
|
||||
completion(.failure(ModuleError.duplicateModule))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func deleteModule(named name: String) {
|
||||
if let index = modules.firstIndex(where: { $0.name == name }) {
|
||||
modules.remove(at: index)
|
||||
moduleURLs.removeValue(forKey: name)
|
||||
saveModuleData()
|
||||
saveModuleURLs()
|
||||
NotificationCenter.default.post(name: .moduleRemoved, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func refreshModules() {
|
||||
for (name, urlString) in moduleURLs {
|
||||
guard let url = URL(string: urlString) else { continue }
|
||||
let task = URLSession.custom.dataTask(with: url) { data, _, error in
|
||||
guard let data = data, error == nil else { return }
|
||||
do {
|
||||
let updatedModule = try JSONDecoder().decode(ModuleStruct.self, from: data)
|
||||
DispatchQueue.main.async {
|
||||
if let index = self.modules.firstIndex(where: { $0.name == name }) {
|
||||
self.modules[index] = updatedModule
|
||||
self.saveModuleData()
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Failed to decode module during refresh: \(error.localizedDescription)")
|
||||
Logger.shared.log("Failed to decode module during refresh: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
private func loadModuleURLs() {
|
||||
let fileURL = getDocumentsDirectory().appendingPathComponent(moduleURLsFileName)
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
moduleURLs = try JSONDecoder().decode([String: String].self, from: data)
|
||||
} catch {
|
||||
print("Failed to load module URLs: \(error.localizedDescription)")
|
||||
Logger.shared.log("Failed to load module URLs: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
private func loadModuleData() {
|
||||
let fileURL = getDocumentsDirectory().appendingPathComponent(modulesFileName)
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
modules = try JSONDecoder().decode([ModuleStruct].self, from: data)
|
||||
} catch {
|
||||
print("Failed to load modules: \(error.localizedDescription)")
|
||||
Logger.shared.log("Failed to load modules: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
func saveModuleData() {
|
||||
let fileURL = getDocumentsDirectory().appendingPathComponent(modulesFileName)
|
||||
do {
|
||||
let data = try JSONEncoder().encode(modules)
|
||||
try data.write(to: fileURL)
|
||||
} catch {
|
||||
print("Failed to save modules: \(error.localizedDescription)")
|
||||
Logger.shared.log("Failed to save modules: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
private func saveModuleURLs() {
|
||||
let fileURL = getDocumentsDirectory().appendingPathComponent(moduleURLsFileName)
|
||||
do {
|
||||
let data = try JSONEncoder().encode(moduleURLs)
|
||||
try data.write(to: fileURL)
|
||||
} catch {
|
||||
print("Failed to save module URLs: \(error.localizedDescription)")
|
||||
Logger.shared.log("Failed to save module URLs: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
private func getDocumentsDirectory() -> URL {
|
||||
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
}
|
||||
|
||||
enum ModuleError: LocalizedError {
|
||||
case invalidURL
|
||||
case duplicateModule
|
||||
case unknown
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidURL:
|
||||
return "The provided URL is invalid."
|
||||
case .duplicateModule:
|
||||
return "This module already exists."
|
||||
case .unknown:
|
||||
return "An unknown error occurred."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
override var prefersHomeIndicatorAutoHidden: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override var prefersStatusBarHidden: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
//
|
||||
// VideoPlayerView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AVKit
|
||||
|
||||
class VideoPlayerViewController: UIViewController {
|
||||
let module: ModuleStruct
|
||||
|
||||
var player: AVPlayer?
|
||||
var playerViewController: AVPlayerViewController?
|
||||
var timeObserverToken: Any?
|
||||
var streamUrl: String?
|
||||
var fullUrl: String = ""
|
||||
|
||||
init(module: ModuleStruct) {
|
||||
self.module = module
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
guard let streamUrl = streamUrl, let url = URL(string: streamUrl) else {
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
if streamUrl.contains("ascdn") {
|
||||
request.addValue("\(module.module[0].details.baseURL)", forHTTPHeaderField: "Referer")
|
||||
}
|
||||
|
||||
let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]])
|
||||
let playerItem = AVPlayerItem(asset: asset)
|
||||
|
||||
player = AVPlayer(playerItem: playerItem)
|
||||
playerViewController = NormalPlayer()
|
||||
playerViewController?.player = player
|
||||
addPeriodicTimeObserver(fullURL: fullUrl)
|
||||
|
||||
if let playerViewController = playerViewController {
|
||||
playerViewController.view.frame = self.view.frame
|
||||
self.view.addSubview(playerViewController.view)
|
||||
self.addChild(playerViewController)
|
||||
playerViewController.didMove(toParent: self)
|
||||
}
|
||||
|
||||
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(fullUrl)")
|
||||
if lastPlayedTime > 0 {
|
||||
let seekTime = CMTime(seconds: lastPlayedTime, preferredTimescale: 1)
|
||||
self.player?.seek(to: seekTime) { _ in
|
||||
self.player?.play()
|
||||
}
|
||||
} else {
|
||||
self.player?.play()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
player?.play()
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
player?.pause()
|
||||
if let timeObserverToken = timeObserverToken {
|
||||
player?.removeTimeObserver(timeObserverToken)
|
||||
self.timeObserverToken = nil
|
||||
}
|
||||
}
|
||||
|
||||
func addPeriodicTimeObserver(fullURL: String) {
|
||||
guard let player = self.player else { return }
|
||||
|
||||
let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||
timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in
|
||||
guard let currentItem = player.currentItem,
|
||||
currentItem.duration.seconds.isFinite else {
|
||||
return
|
||||
}
|
||||
|
||||
let currentTime = time.seconds
|
||||
let duration = currentItem.duration.seconds
|
||||
|
||||
UserDefaults.standard.set(currentTime, forKey: "lastPlayedTime_\(fullURL)")
|
||||
UserDefaults.standard.set(duration, forKey: "totalTime_\(fullURL)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
//
|
||||
// HomeView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import SwiftSoup
|
||||
|
||||
struct HomeView: View {
|
||||
@StateObject private var modulesManager = ModulesManager()
|
||||
@State private var featuredItems: [String: [ItemResult]] = [:]
|
||||
@State private var isLoading = true
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
if isLoading {
|
||||
ProgressView("Loading Featured Items...")
|
||||
.padding()
|
||||
} else {
|
||||
ForEach(modulesManager.modules, id: \.name) { module in
|
||||
if let items = featuredItems[module.name], !items.isEmpty {
|
||||
VStack(alignment: .leading) {
|
||||
HStack(alignment: .bottom) {
|
||||
Text("Featured")
|
||||
.font(.title2)
|
||||
.bold()
|
||||
.padding(.leading)
|
||||
|
||||
Text("on \(module.name)")
|
||||
.font(.system(size: 15))
|
||||
.foregroundColor(.secondary)
|
||||
.bold()
|
||||
}
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 20) {
|
||||
ForEach(items) { item in
|
||||
NavigationLink(destination: MediaView(module: module, item: item)) {
|
||||
VStack {
|
||||
KFImage(URL(string: item.imageUrl))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 120, height: 180)
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
|
||||
Text(item.name)
|
||||
.font(.caption)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
.frame(width: 120)
|
||||
.padding(.leading, 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Home")
|
||||
}
|
||||
.onAppear {
|
||||
if featuredItems.isEmpty {
|
||||
fetchFeaturedItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
|
||||
private func fetchFeaturedItems() {
|
||||
isLoading = true
|
||||
let group = DispatchGroup()
|
||||
|
||||
for module in modulesManager.modules {
|
||||
group.enter()
|
||||
fetchFeaturedItems(for: module) { items in
|
||||
DispatchQueue.main.async {
|
||||
featuredItems[module.name] = items
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: .main) {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchFeaturedItems(for module: ModuleStruct, completion: @escaping ([ItemResult]) -> Void) {
|
||||
let urlString = module.module[0].featured.url
|
||||
guard let url = URL(string: urlString) else {
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
|
||||
URLSession.custom.dataTask(with: url) { data, _, error in
|
||||
guard let data = data, error == nil else {
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let html = String(data: data, encoding: .utf8) ?? ""
|
||||
let document = try SwiftSoup.parse(html)
|
||||
let elements = try document.select(module.module[0].featured.documentSelector)
|
||||
|
||||
var results: [ItemResult] = []
|
||||
for element in elements {
|
||||
let title = try element.select(module.module[0].featured.title).text()
|
||||
let href = try element.select(module.module[0].featured.href).attr("href")
|
||||
var imageURL = try element.select(module.module[0].featured.image.url).attr(module.module[0].featured.image.attribute)
|
||||
|
||||
if imageURL.contains(",") {
|
||||
imageURL = imageURL.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.first ?? imageURL
|
||||
}
|
||||
|
||||
if !imageURL.starts(with: "http") {
|
||||
imageURL = "\(module.module[0].details.baseURL.hasSuffix("/") ? module.module[0].details.baseURL : "\(module.module[0].details.baseURL)/")\(imageURL.hasPrefix("/") ? String(imageURL.dropFirst()) : imageURL)"
|
||||
}
|
||||
|
||||
imageURL = imageURL.replacingOccurrences(of: " ", with: "%20")
|
||||
|
||||
let result = ItemResult(name: title, imageUrl: imageURL, href: href)
|
||||
results.append(result)
|
||||
}
|
||||
|
||||
completion(results)
|
||||
} catch {
|
||||
print("Error parsing HTML: \(error)")
|
||||
Logger.shared.log("Error parsing HTML: \(error)")
|
||||
completion([])
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
//
|
||||
// LibraryManager.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class LibraryManager: ObservableObject {
|
||||
static let shared = LibraryManager()
|
||||
|
||||
@Published var libraryItems: [LibraryItem] = []
|
||||
private let userDefaults = UserDefaults.standard
|
||||
private let libraryKey = "LibraryItems"
|
||||
|
||||
private init() {
|
||||
loadLibrary()
|
||||
}
|
||||
|
||||
func loadLibrary() {
|
||||
if let data = userDefaults.data(forKey: libraryKey),
|
||||
let decoded = try? JSONDecoder().decode([LibraryItem].self, from: data) {
|
||||
libraryItems = decoded
|
||||
}
|
||||
}
|
||||
|
||||
func saveLibrary() {
|
||||
if let encoded = try? JSONEncoder().encode(libraryItems) {
|
||||
userDefaults.set(encoded, forKey: libraryKey)
|
||||
}
|
||||
}
|
||||
|
||||
func addToLibrary(_ item: LibraryItem) {
|
||||
if !libraryItems.contains(where: { $0.id == item.id }) {
|
||||
libraryItems.append(item)
|
||||
saveLibrary()
|
||||
Logger.shared.log("Added to library: \(item.title)")
|
||||
}
|
||||
}
|
||||
|
||||
func removeFromLibrary(_ item: LibraryItem) {
|
||||
libraryItems.removeAll(where: { $0.id == item.id })
|
||||
saveLibrary()
|
||||
Logger.shared.log("Removed from library: \(item.title)")
|
||||
}
|
||||
|
||||
func importFromMiruData(_ miruData: MiruDataStruct, module: ModuleStruct) {
|
||||
var newLibraryItems: [LibraryItem] = []
|
||||
|
||||
for like in miruData.likes {
|
||||
let libraryItem = LibraryItem(
|
||||
anilistID: like.anilistID,
|
||||
title: like.title,
|
||||
image: like.cover,
|
||||
url: like.gogoSlug,
|
||||
module: module,
|
||||
dateAdded: Date()
|
||||
)
|
||||
newLibraryItems.append(libraryItem)
|
||||
Logger.shared.log("Importing item: \(libraryItem.title)")
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.libraryItems.append(contentsOf: newLibraryItems)
|
||||
self.saveLibrary()
|
||||
Logger.shared.log("Completed importing \(newLibraryItems.count) items")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
//
|
||||
// LibraryView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import Foundation
|
||||
|
||||
struct LibraryItem: Identifiable, Codable {
|
||||
var id = UUID()
|
||||
let anilistID: Int
|
||||
let title: String
|
||||
let image: String
|
||||
let url: String
|
||||
let module: ModuleStruct
|
||||
var dateAdded: Date
|
||||
}
|
||||
|
||||
struct LibraryView: View {
|
||||
@StateObject private var libraryManager = LibraryManager.shared
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ScrollView {
|
||||
if libraryManager.libraryItems.isEmpty {
|
||||
emptyLibraryView
|
||||
} else {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))], spacing: 20) {
|
||||
ForEach(libraryManager.libraryItems.sorted(by: { $0.dateAdded > $1.dateAdded })) { item in
|
||||
NavigationLink(destination: MediaView(module: item.module, item: ItemResult(name: item.title, imageUrl: item.image, href: item.url))) {
|
||||
itemView(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.navigationTitle("Library")
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
|
||||
var emptyLibraryView: some View {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "books.vertical")
|
||||
.font(.system(size: 75))
|
||||
.foregroundColor(.secondary)
|
||||
Text("Your library is empty")
|
||||
.font(.headline)
|
||||
.multilineTextAlignment(.center)
|
||||
Text("Start by adding items you find in the search results or by importing Miru bookmarks from settings!")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.position(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height / 3)
|
||||
}
|
||||
|
||||
func itemView(_ item: LibraryItem) -> some View {
|
||||
VStack() {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
KFImage(URL(string: item.image))
|
||||
.resizable()
|
||||
.aspectRatio(2/3, contentMode: .fill)
|
||||
.cornerRadius(10)
|
||||
.frame(width: 150, height: 225)
|
||||
|
||||
KFImage(URL(string: item.module.iconURL))
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 30, height: 30)
|
||||
.background(Color.white)
|
||||
.clipShape(Circle())
|
||||
.padding(5)
|
||||
}
|
||||
|
||||
Text(item.title)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.primary)
|
||||
.padding([.leading, .bottom], 8)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(role: .destructive) {
|
||||
libraryManager.removeFromLibrary(item)
|
||||
} label: {
|
||||
Label("Remove from Library", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
//
|
||||
// CircularProgressBar.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CircularProgressBar: View {
|
||||
var progress: Double
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(lineWidth: 5.0)
|
||||
.opacity(0.3)
|
||||
.foregroundColor(Color.accentColor)
|
||||
|
||||
Circle()
|
||||
.trim(from: 0.0, to: CGFloat(min(progress, 1.0)))
|
||||
.stroke(style: StrokeStyle(lineWidth: 5.0, lineCap: .round, lineJoin: .round))
|
||||
.foregroundColor(Color.accentColor)
|
||||
.rotationEffect(Angle(degrees: 270.0))
|
||||
.animation(.linear, value: progress)
|
||||
|
||||
if progress >= 0.90 {
|
||||
Image(systemName: "checkmark")
|
||||
.font(.system(size: 12))
|
||||
} else {
|
||||
Text(String(format: "%.0f%%", min(progress, 1.0) * 100.0))
|
||||
.font(.system(size: 12))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
//
|
||||
// EpisodeCell.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import SwiftyJSON
|
||||
|
||||
struct EpisodeCell: View {
|
||||
let episode: String
|
||||
let episodeID: Int
|
||||
let imageUrl: String
|
||||
let progress: Double
|
||||
let itemID: Int
|
||||
|
||||
@State private var episodeTitle: String = ""
|
||||
@State private var episodeImageUrl: String = ""
|
||||
@State private var isLoading: Bool = true
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
ZStack {
|
||||
KFImage(URL(string: episodeImageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : episodeImageUrl))
|
||||
.resizable()
|
||||
.aspectRatio(16/9, contentMode: .fill)
|
||||
.frame(width: 100, height: 56)
|
||||
.cornerRadius(8)
|
||||
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle())
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("Episode \(episodeID + 1)")
|
||||
.font(.system(size: 15))
|
||||
if !episodeTitle.isEmpty {
|
||||
Text(episodeTitle)
|
||||
.font(.system(size: 13))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
CircularProgressBar(progress: progress)
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
.onAppear {
|
||||
fetchEpisodeDetails()
|
||||
}
|
||||
}
|
||||
|
||||
func fetchEpisodeDetails() {
|
||||
let cacheKey = "episodeDetails_\(itemID)_\(episodeID)"
|
||||
|
||||
if let cachedData = UserDefaults.standard.data(forKey: cacheKey) {
|
||||
parseEpisodeDetails(data: cachedData)
|
||||
return
|
||||
}
|
||||
|
||||
guard let url = URL(string: "https://api.ani.zip/mappings?anilist_id=\(itemID)") else {
|
||||
isLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
URLSession.custom.dataTask(with: url) { data, _, error in
|
||||
if let error = error {
|
||||
print("Failed to fetch episode details: \(error)")
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
print("No data received")
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
UserDefaults.standard.set(data, forKey: cacheKey)
|
||||
self.parseEpisodeDetails(data: data)
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func parseEpisodeDetails(data: Data) {
|
||||
do {
|
||||
let json = try JSON(data: data)
|
||||
guard let episodeDetails = json["episodes"]["\(episodeID + 1)"].dictionary,
|
||||
let title = episodeDetails["title"]?.dictionary,
|
||||
let image = episodeDetails["image"]?.string else {
|
||||
print("Invalid response format")
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.episodeTitle = title["en"]?.string ?? ""
|
||||
self.episodeImageUrl = image
|
||||
self.isLoading = false
|
||||
}
|
||||
} catch {
|
||||
print("Failed to parse JSON: \(error)")
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,403 +0,0 @@
|
|||
//
|
||||
// MediaExtraction.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftSoup
|
||||
|
||||
extension MediaView {
|
||||
func fetchItemDetails() {
|
||||
guard let url = URL(string: item.href.hasPrefix("https") ? item.href : "\(module.module[0].details.baseURL.hasSuffix("/") ? module.module[0].details.baseURL : "\(module.module[0].details.baseURL)/")\(item.href.hasPrefix("/") ? String(item.href.dropFirst()) : item.href)") else { return }
|
||||
|
||||
URLSession.custom.dataTask(with: url) { data, _, error in
|
||||
defer { isLoading = false }
|
||||
guard let data = data, error == nil else { return }
|
||||
|
||||
do {
|
||||
let html = String(data: data, encoding: .utf8) ?? ""
|
||||
let document = try SwiftSoup.parse(html)
|
||||
|
||||
let details = module.module[0].details
|
||||
let episodes = module.module[0].episodes
|
||||
|
||||
let aliases = (try? document.select(details.aliases.selector).attr(details.aliases.attribute)) ?? ""
|
||||
let synopsis = (try? document.select(details.synopsis).text()) ?? ""
|
||||
let airdate = (try? document.select(details.airdate).text()) ?? ""
|
||||
let stars = (try? document.select(details.stars).text()) ?? ""
|
||||
|
||||
let episodeElements = try document.select(episodes.selector)
|
||||
var episodeList = (try? episodeElements.map { try $0.attr("href") }) ?? []
|
||||
|
||||
if module.module[0].episodes.order == "reversed" {
|
||||
episodeList.reverse()
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.aliases = aliases
|
||||
self.synopsis = synopsis
|
||||
self.airdate = airdate
|
||||
self.stars = stars
|
||||
self.episodes = episodeList
|
||||
}
|
||||
} catch {
|
||||
print("Error parsing HTML: \(error)")
|
||||
Logger.shared.log("Error parsing HTML: \(error)")
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func fetchEpisodeStream(urlString: String) {
|
||||
guard var url = URL(string: urlString.hasPrefix("https") ? urlString : "\(module.module[0].details.baseURL)\(urlString)") else { return }
|
||||
|
||||
Logger.shared.log("Pressed episode button")
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
let pageRedirects = module.module[0].details.pageRedirects ?? false
|
||||
|
||||
|
||||
if pageRedirects {
|
||||
dispatchGroup.enter() // Start tracking the redirect task
|
||||
URLSession.custom.dataTask(with: url) { data, _, error in
|
||||
guard let data = data, error == nil else {
|
||||
dispatchGroup.leave() // End tracking if there's an error
|
||||
return
|
||||
}
|
||||
|
||||
let html = String(data: data, encoding: .utf8) ?? ""
|
||||
let redirectedUrl = extractFromRedirectURL(from: html)
|
||||
if let redirect = redirectedUrl, let newURL = URL(string: redirect) {
|
||||
url = newURL
|
||||
}
|
||||
dispatchGroup.leave() // End tracking after successful execution
|
||||
}.resume()
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .main) { // This block executes after all tasks
|
||||
URLSession.custom.dataTask(with: url) { data, _, error in
|
||||
guard let data = data, error == nil else { return }
|
||||
|
||||
let html = String(data: data, encoding: .utf8) ?? ""
|
||||
|
||||
|
||||
let streamType = module.stream
|
||||
let streamURLs = extractStreamURLs(from: html, streamType: streamType)
|
||||
|
||||
if module.extractor == "dub-sub" {
|
||||
Logger.shared.log("extracting for dub-sub")
|
||||
let dubSubURLs = extractDubSubURLs(from: html)
|
||||
let subURLs = dubSubURLs.filter { $0.type == "SUB" }.map { $0.url }
|
||||
let dubURLs = dubSubURLs.filter { $0.type == "DUB" }.map { $0.url }
|
||||
|
||||
if !subURLs.isEmpty || !dubURLs.isEmpty {
|
||||
DispatchQueue.main.async {
|
||||
self.presentStreamSelection(subURLs: subURLs, dubURLs: dubURLs, fullURL: urlString)
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.playStream(urlString: streamURLs.first, fullURL: urlString)
|
||||
}
|
||||
}
|
||||
} else if module.extractor == "pattern-mp4" || module.extractor == "pattern-HLS" {
|
||||
Logger.shared.log("extracting for pattern-mp4/hls")
|
||||
let patternURL = extractPatternURL(from: html)
|
||||
guard let patternURL = patternURL else { return }
|
||||
|
||||
URLSession.custom.dataTask(with: patternURL) { data, _, error in
|
||||
guard let data = data, error == nil else { return }
|
||||
|
||||
let patternHTML = String(data: data, encoding: .utf8) ?? ""
|
||||
let mp4URLs = extractStreamURLs(from: patternHTML, streamType: streamType).map { $0.replacingOccurrences(of: "amp;", with: "") }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.playStream(urlString: mp4URLs.first, fullURL: urlString)
|
||||
}
|
||||
}.resume()
|
||||
} else if module.extractor == "pattern" {
|
||||
Logger.shared.log("extracting for pattern")
|
||||
let patternURL = extractPatternURL(from: html)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.playStream(urlString: patternURL?.absoluteString, fullURL: urlString)
|
||||
}
|
||||
} else if module.extractor == "voe" {
|
||||
Logger.shared.log("extracting for voe")
|
||||
|
||||
let voeUrl = extractVoeStream(from: html)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.playStream(urlString: voeUrl?.absoluteString, fullURL: urlString)
|
||||
}
|
||||
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.playStream(urlString: streamURLs.first, fullURL: urlString)
|
||||
}
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func extractStreamURLs(from html: String, streamType: String) -> [String] {
|
||||
let pattern: String
|
||||
switch streamType {
|
||||
case "HLS":
|
||||
pattern = #"https:\/\/[^"\s<>]+\.m3u8(?:\?[^\s"'<>]+)?"#
|
||||
case "MP4":
|
||||
pattern = #"https:\/\/[^"\s<>]+\.mp4(?:\?[^\s"'<>]+)?"#
|
||||
default:
|
||||
return []
|
||||
}
|
||||
|
||||
do {
|
||||
Logger.shared.log(streamType)
|
||||
let regex = try NSRegularExpression(pattern: pattern, options: [])
|
||||
let matches = regex.matches(in: html, options: [], range: NSRange(html.startIndex..., in: html))
|
||||
return matches.compactMap {
|
||||
Range($0.range, in: html).map { String(html[$0]) }
|
||||
}
|
||||
} catch {
|
||||
print("Invalid regex: \(error)")
|
||||
Logger.shared.log("Invalid regex: \(error)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
func extractPatternURL(from html: String) -> URL? {
|
||||
var pattern = module.module[0].episodes.pattern
|
||||
|
||||
if module.extractor == "pattern" {
|
||||
if let data = Data(base64Encoded: pattern), let decodedPattern = String(data: data, encoding: .utf8) {
|
||||
pattern = decodedPattern
|
||||
} else {
|
||||
print("Failed to decode base64 pattern")
|
||||
Logger.shared.log("Failed to decode base64 pattern")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
let regex = try NSRegularExpression(pattern: pattern, options: [])
|
||||
let range = NSRange(html.startIndex..<html.endIndex, in: html)
|
||||
|
||||
if let match = regex.firstMatch(in: html, options: [], range: range),
|
||||
let matchRange = Range(match.range, in: html) {
|
||||
var urlString = String(html[matchRange])
|
||||
urlString = urlString.replacingOccurrences(of: "amp;", with: "")
|
||||
urlString = urlString.replacingOccurrences(of: "\"", with: "")
|
||||
|
||||
if let httpsRange = urlString.range(of: "https") {
|
||||
urlString = String(urlString[httpsRange.lowerBound...])
|
||||
}
|
||||
|
||||
return URL(string: urlString)
|
||||
}
|
||||
} catch {
|
||||
print("Invalid regex: \(error)")
|
||||
Logger.shared.log("Invalid regex: \(error)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// Grabs hls stream from voe sites
|
||||
func extractVoeStream(from html: String) -> URL? {
|
||||
|
||||
let hlsPattern = "'hls': '(.*?)'"
|
||||
guard let regex = try? NSRegularExpression(pattern: hlsPattern, options: []) else { return nil }
|
||||
let range = NSRange(html.startIndex..., in: html)
|
||||
if let match = regex.firstMatch(in: html, options: [], range: range),
|
||||
let matchRange = Range(match.range(at: 1), in: html) {
|
||||
let base64Hls = String(html[matchRange])
|
||||
guard let data = Data(base64Encoded: base64Hls),
|
||||
let decodedURLString = String(data: data, encoding: .utf8)
|
||||
else { return nil }
|
||||
return URL(string: decodedURLString)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func presentStreamSelection(subURLs: [String], dubURLs: [String], fullURL: String) {
|
||||
let uniqueSubURLs = Array(Set(subURLs))
|
||||
let uniqueDubURLs = Array(Set(dubURLs))
|
||||
|
||||
if uniqueSubURLs.count == 1 && uniqueDubURLs.isEmpty {
|
||||
self.playStream(urlString: uniqueSubURLs.first, fullURL: fullURL)
|
||||
return
|
||||
}
|
||||
|
||||
if uniqueDubURLs.count == 1 && uniqueSubURLs.isEmpty {
|
||||
self.playStream(urlString: uniqueDubURLs.first, fullURL: fullURL)
|
||||
return
|
||||
}
|
||||
|
||||
let alert = UIAlertController(title: "Select Stream", message: "Choose the audio type", preferredStyle: .actionSheet)
|
||||
|
||||
if !uniqueDubURLs.isEmpty {
|
||||
for dubURL in uniqueDubURLs {
|
||||
alert.addAction(UIAlertAction(title: "DUB", style: .default) { _ in
|
||||
self.playStream(urlString: dubURL, fullURL: fullURL)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !uniqueSubURLs.isEmpty {
|
||||
for subURL in uniqueSubURLs {
|
||||
alert.addAction(UIAlertAction(title: "SUB", style: .default) { _ in
|
||||
self.playStream(urlString: subURL, fullURL: fullURL)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
if let popoverController = alert.popoverPresentationController {
|
||||
popoverController.sourceView = rootVC.view
|
||||
popoverController.sourceRect = CGRect(x: rootVC.view.bounds.midX, y: rootVC.view.bounds.midY, width: 0, height: 0)
|
||||
popoverController.permittedArrowDirections = []
|
||||
}
|
||||
rootVC.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Extracts the URL from a redirect page
|
||||
/// Example: href="/redirect/1234567" -> https://baseUrl.com/redirect/1234567
|
||||
func extractFromRedirectURL(from html: String) -> String? {
|
||||
|
||||
let pattern = #"href="\/redirect\/\d+""#
|
||||
|
||||
do {
|
||||
let regex = try NSRegularExpression(pattern: pattern, options: [])
|
||||
let range = NSRange(html.startIndex..<html.endIndex, in: html)
|
||||
|
||||
if let match = regex.firstMatch(in: html, options: [], range: range),
|
||||
let matchRange = Range(match.range, in: html) {
|
||||
var urlString = String(html[matchRange])
|
||||
urlString = urlString.replacingOccurrences(of: "href=\"", with: "")
|
||||
urlString = urlString.replacingOccurrences(of: "\"", with: "")
|
||||
|
||||
// Ensure the baseURL ends with "/" before appending the path
|
||||
let baseURL = module.module[0].details.baseURL
|
||||
|
||||
let redirectUrl = baseURL + urlString
|
||||
|
||||
let finalUrl = fetchRedirectedURLFromHeader(url: URL(string: redirectUrl)!)
|
||||
|
||||
return finalUrl
|
||||
}
|
||||
} catch {
|
||||
print("Invalid regex: \(error)")
|
||||
Logger.shared.log("Invalid regex: \(error)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Fetches the redirected URL from the header of a given URL
|
||||
/// Header Parameter: Location
|
||||
func fetchRedirectedURLFromHeader(url: URL) -> String? {
|
||||
let semaphore = DispatchSemaphore(value: 0) // To block the thread until the task completes
|
||||
var redirectedURLString: String?
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "HEAD" // Use HEAD to get only headers
|
||||
|
||||
let delegate = RedirectHandler()
|
||||
let sessionConfig = URLSessionConfiguration.default
|
||||
let session = URLSession(configuration: sessionConfig, delegate: delegate, delegateQueue: nil)
|
||||
|
||||
session.dataTask(with: request) { _, response, error in
|
||||
// Extract httpResponse as a standalone variable
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
Logger.shared.log("Invalid response for URL: \(url)")
|
||||
semaphore.signal()
|
||||
return
|
||||
}
|
||||
|
||||
// Process the httpResponse for redirection logic
|
||||
if (httpResponse.statusCode == 301 || httpResponse.statusCode == 302),
|
||||
let location = httpResponse.value(forHTTPHeaderField: "Location"),
|
||||
let redirectedURL = URL(string: location) {
|
||||
redirectedURLString = redirectedURL.absoluteString
|
||||
Logger.shared.log("Redirected URL: \(redirectedURLString ?? "nil")")
|
||||
} else {
|
||||
if let error = error {
|
||||
Logger.shared.log("Error fetching redirected URL: \(error.localizedDescription)")
|
||||
} else {
|
||||
Logger.shared.log("No redirection for URL: \(url)")
|
||||
}
|
||||
}
|
||||
semaphore.signal() // Signal the semaphore to resume execution
|
||||
}.resume()
|
||||
|
||||
semaphore.wait() // Wait for the network task to complete
|
||||
|
||||
if redirectedURLString?.contains("voe.sx") == true {
|
||||
return voeUrlHandler(url: URL(string: redirectedURLString!)!)
|
||||
}
|
||||
else {
|
||||
return redirectedURLString
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Voe uses a custom handler to extract the video URL from the page
|
||||
/// The site uses windows.location.href to redirect to the video page, usally another domain but with the same path
|
||||
/// The replacement URL is hardcoded right now TODO: Make it dynamic
|
||||
func voeUrlHandler(url: URL) -> String? {
|
||||
|
||||
let urlString = url.absoluteString
|
||||
|
||||
// Check if the URL is a voe.sx URL
|
||||
guard urlString.contains("voe.sx") else {
|
||||
Logger.shared.log("Not a voe.sx URL")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Extract the path from the URL and append it to the hardcoded replacement URL
|
||||
// Example: https://voe.sx/e/1234567 -> /e/1234567
|
||||
let hardcodedURL = "https://sandratableother.com"
|
||||
let finishedUrl = urlString.replacingOccurrences(of: "https://voe.sx", with: hardcodedURL)
|
||||
|
||||
return finishedUrl
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Custom handler to handle HTTP redirections and prevent them
|
||||
class RedirectHandler: NSObject, URLSessionDelegate, URLSessionTaskDelegate {
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping @Sendable (URLRequest?) -> Void
|
||||
) {
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,376 +0,0 @@
|
|||
//
|
||||
// MediaView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import AVKit
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import SwiftyJSON
|
||||
import SafariServices
|
||||
|
||||
struct MediaView: View {
|
||||
let module: ModuleStruct
|
||||
let item: ItemResult
|
||||
|
||||
@State var aliases: String = ""
|
||||
@State var synopsis: String = ""
|
||||
@State var airdate: String = ""
|
||||
@State var stars: String = ""
|
||||
@State var episodes: [String] = []
|
||||
@State var isLoading: Bool = true
|
||||
@State var showFullSynopsis: Bool = false
|
||||
@State var itemID: Int?
|
||||
@State private var selectedEpisode: String = ""
|
||||
@State private var selectedEpisodeNumber: Int = 0
|
||||
@State private var episodeRange: ClosedRange<Int> = 0...99
|
||||
@State private var selectedRange: String = "1-100"
|
||||
|
||||
@AppStorage("externalPlayer") private var externalPlayer: String = "Default"
|
||||
@StateObject private var libraryManager = LibraryManager.shared
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.padding()
|
||||
} else {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
KFImage(URL(string: item.imageUrl))
|
||||
.resizable()
|
||||
.aspectRatio(2/3, contentMode: .fill)
|
||||
.cornerRadius(10)
|
||||
.frame(width: 150, height: 225)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(item.name)
|
||||
.font(.system(size: 17))
|
||||
.fontWeight(.bold)
|
||||
|
||||
if !aliases.isEmpty && aliases != item.name {
|
||||
Text(aliases)
|
||||
.font(.system(size: 13))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(alignment: .center, spacing: 12) {
|
||||
Text(module.name)
|
||||
.font(.system(size: 13))
|
||||
.padding(4)
|
||||
.background(Capsule().fill(Color.accentColor.opacity(0.4)))
|
||||
|
||||
Button(action: {
|
||||
}) {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
openSafariViewController(with: "\(module.module[0].details.baseURL)")
|
||||
}) {
|
||||
Image(systemName: "safari")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !synopsis.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack(alignment: .center) {
|
||||
Text("Synopsis")
|
||||
.font(.system(size: 18))
|
||||
.fontWeight(.bold)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
showFullSynopsis.toggle()
|
||||
}) {
|
||||
Text(showFullSynopsis ? "Less" : "More")
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
}
|
||||
|
||||
Text(synopsis)
|
||||
.lineLimit(showFullSynopsis ? nil : 4)
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Button(action: {
|
||||
startWatchingFirstUnfinishedEpisode()
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "play.fill")
|
||||
.foregroundColor(.primary)
|
||||
Text("Start Watching")
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.accentColor)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
if isItemInLibrary() {
|
||||
removeFromLibrary()
|
||||
} else {
|
||||
addToLibrary()
|
||||
}
|
||||
}) {
|
||||
Image(systemName: isItemInLibrary() ? "bookmark.fill" : "bookmark")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 27)
|
||||
}
|
||||
}
|
||||
|
||||
if !episodes.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
Text("Episodes")
|
||||
.font(.system(size: 18))
|
||||
.fontWeight(.bold)
|
||||
|
||||
Spacer()
|
||||
|
||||
if episodes.count > 100 {
|
||||
Menu {
|
||||
ForEach(0..<(episodes.count / 100) + 1, id: \.self) { index in
|
||||
let start = index * 100 + 1
|
||||
let end = min((index + 1) * 100, episodes.count)
|
||||
Button(action: {
|
||||
episodeRange = (start - 1)...(end - 1)
|
||||
selectedRange = "\(start)-\(end)"
|
||||
}) {
|
||||
Text("\(start)-\(end)")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(selectedRange)
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(episodeRange, id: \.self) { index in
|
||||
if index < episodes.count {
|
||||
let episodeURL = episodes[index].hasPrefix("https") ? episodes[index] : "\(module.module[0].details.baseURL)\(episodes[index])"
|
||||
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(episodeURL)")
|
||||
let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(episodeURL)")
|
||||
let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0
|
||||
|
||||
EpisodeCell(episode: episodes[index], episodeID: index, imageUrl: item.imageUrl, progress: progress, itemID: itemID ?? 0)
|
||||
.onTapGesture {
|
||||
selectedEpisode = episodes[index]
|
||||
selectedEpisodeNumber = index + 1
|
||||
fetchEpisodeStream(urlString: episodeURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarTitle(item.name)
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
fetchItemDetails()
|
||||
fetchItemID(byTitle: item.name) { result in
|
||||
switch result {
|
||||
case .success(let id):
|
||||
itemID = id
|
||||
Logger.shared.log("Fetched Item ID: \(id)")
|
||||
case .failure(let error):
|
||||
print("Failed to fetch Item ID: \(error)")
|
||||
Logger.shared.log("Failed to fetch Item ID: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isItemInLibrary() -> Bool {
|
||||
return libraryManager.libraryItems.contains(where: { $0.url == item.href })
|
||||
}
|
||||
|
||||
func addToLibrary() {
|
||||
let libraryItem = LibraryItem(
|
||||
anilistID: itemID ?? 0,
|
||||
title: item.name,
|
||||
image: item.imageUrl,
|
||||
url: item.href,
|
||||
module: module,
|
||||
dateAdded: Date()
|
||||
)
|
||||
libraryManager.addToLibrary(libraryItem)
|
||||
}
|
||||
|
||||
func removeFromLibrary() {
|
||||
if let libraryItem = libraryManager.libraryItems.first(where: { $0.url == item.href }) {
|
||||
libraryManager.removeFromLibrary(libraryItem)
|
||||
}
|
||||
}
|
||||
|
||||
private func startWatchingFirstUnfinishedEpisode() {
|
||||
for (index, episode) in episodes.enumerated() {
|
||||
let episodeURL = episode.hasPrefix("https") ? episode : "\(module.module[0].details.baseURL)\(episode)"
|
||||
let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(episodeURL)")
|
||||
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(episodeURL)")
|
||||
let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0
|
||||
|
||||
if progress < 0.90 {
|
||||
selectedEpisode = episode
|
||||
selectedEpisodeNumber = index + 1
|
||||
fetchEpisodeStream(urlString: episodeURL)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func playStream(urlString: String?, fullURL: String) {
|
||||
guard let streamUrl = urlString else { return }
|
||||
|
||||
if externalPlayer == "Infuse" || externalPlayer == "VLC" || externalPlayer == "OutPlayer" || externalPlayer == "nPlayer" {
|
||||
var scheme: String
|
||||
switch externalPlayer {
|
||||
case "Infuse":
|
||||
scheme = "infuse://x-callback-url/play?url="
|
||||
case "VLC":
|
||||
scheme = "vlc://"
|
||||
case "OutPlayer":
|
||||
scheme = "outplayer://"
|
||||
case "nPlayer":
|
||||
scheme = "nplayer-"
|
||||
default:
|
||||
scheme = ""
|
||||
}
|
||||
openInExternalPlayer(scheme: scheme, url: streamUrl)
|
||||
Logger.shared.log("Opening external app with scheme: \(scheme)")
|
||||
return
|
||||
} else if externalPlayer == "Sora" {
|
||||
DispatchQueue.main.async {
|
||||
let customMediaPlayer = CustomMediaPlayer(
|
||||
module: module,
|
||||
urlString: streamUrl,
|
||||
fullUrl: fullURL,
|
||||
title: item.name,
|
||||
episodeNumber: selectedEpisodeNumber,
|
||||
onWatchNext: {
|
||||
selectNextEpisode()
|
||||
}
|
||||
)
|
||||
let hostingController = UIHostingController(rootView: customMediaPlayer)
|
||||
hostingController.modalPresentationStyle = .fullScreen
|
||||
Logger.shared.log("Opening custom media player with url: \(streamUrl)")
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
rootVC.present(hostingController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let videoPlayerViewController = VideoPlayerViewController(module: module)
|
||||
videoPlayerViewController.streamUrl = streamUrl
|
||||
videoPlayerViewController.fullUrl = fullURL
|
||||
videoPlayerViewController.modalPresentationStyle = .fullScreen
|
||||
Logger.shared.log("Opening video player with url: \(streamUrl)")
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
rootVC.present(videoPlayerViewController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func selectNextEpisode() {
|
||||
guard let currentEpisodeIndex = episodes.firstIndex(of: selectedEpisode) else { return }
|
||||
let nextEpisodeIndex = currentEpisodeIndex + 1
|
||||
if nextEpisodeIndex < episodes.count {
|
||||
selectedEpisode = episodes[nextEpisodeIndex]
|
||||
selectedEpisodeNumber = nextEpisodeIndex + 1
|
||||
let nextEpisodeURL = "\(module.module[0].details.baseURL)\(episodes[nextEpisodeIndex])"
|
||||
fetchEpisodeStream(urlString: nextEpisodeURL)
|
||||
}
|
||||
}
|
||||
|
||||
private func openSafariViewController(with urlString: String) {
|
||||
guard let url = URL(string: item.href.hasPrefix("https") ? item.href : "\(module.module[0].details.baseURL.hasSuffix("/") ? module.module[0].details.baseURL : "\(module.module[0].details.baseURL)/")\(item.href.hasPrefix("/") ? String(item.href.dropFirst()) : item.href)") else {
|
||||
Logger.shared.log("Unable to open the webpage")
|
||||
return
|
||||
}
|
||||
let safariViewController = SFSafariViewController(url: url)
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
rootVC.present(safariViewController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func openInExternalPlayer(scheme: String, url: String) {
|
||||
guard let streamUrl = URL(string: "\(scheme)\(url)") else {
|
||||
Logger.shared.log("Unable to open the stream: '\(scheme)\(url)'")
|
||||
return
|
||||
}
|
||||
UIApplication.shared.open(streamUrl, options: [:], completionHandler: nil)
|
||||
Logger.shared.log("Unable to open the stream: 'streamUrl'")
|
||||
}
|
||||
|
||||
func fetchItemID(byTitle title: String, completion: @escaping (Result<Int, Error>) -> Void) {
|
||||
let query = """
|
||||
query {
|
||||
Media(search: "\(title)", type: ANIME) {
|
||||
id
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
guard let url = URL(string: "https://graphql.anilist.co") else {
|
||||
completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])))
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let parameters: [String: Any] = ["query": query]
|
||||
request.httpBody = try? JSONSerialization.data(withJSONObject: parameters)
|
||||
|
||||
URLSession.custom.dataTask(with: request) { data, _, error in
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data received"])))
|
||||
return
|
||||
}
|
||||
|
||||
let json = JSON(data)
|
||||
if let id = json["data"]["Media"]["id"].int {
|
||||
completion(.success(id))
|
||||
} else {
|
||||
let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid response"])
|
||||
completion(.failure(error))
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,236 +0,0 @@
|
|||
//
|
||||
// SearchResultsView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftSoup
|
||||
import Kingfisher
|
||||
|
||||
struct SearchResultsView: View {
|
||||
let module: ModuleStruct?
|
||||
let searchText: String
|
||||
@State private var searchResults: [ItemResult] = []
|
||||
@State private var isLoading: Bool = true
|
||||
@State private var filter: FilterType = .all
|
||||
@AppStorage("listSearch") private var isListSearchEnabled: Bool = false
|
||||
|
||||
enum FilterType: String, CaseIterable {
|
||||
case all = "All"
|
||||
case dub = "Dub"
|
||||
case sub = "Sub"
|
||||
case ova = "OVA"
|
||||
case ona = "ONA"
|
||||
case movie = "Movie"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if isListSearchEnabled {
|
||||
oldUI
|
||||
} else {
|
||||
modernUI
|
||||
}
|
||||
}
|
||||
|
||||
var modernUI: some View {
|
||||
VStack {
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.padding()
|
||||
} else if searchResults.isEmpty {
|
||||
Text("No results found")
|
||||
.foregroundColor(.secondary)
|
||||
.padding()
|
||||
} else {
|
||||
ScrollView {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))], spacing: 20) {
|
||||
ForEach(filteredResults) { result in
|
||||
NavigationLink(destination: MediaView(module: module!, item: result)) {
|
||||
VStack {
|
||||
KFImage(URL(string: result.imageUrl))
|
||||
.resizable()
|
||||
.aspectRatio(2/3, contentMode: .fill)
|
||||
.cornerRadius(10)
|
||||
.frame(width: 150, height: 225)
|
||||
|
||||
Text(result.name)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.primary)
|
||||
.padding([.leading, .bottom], 8)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.navigationTitle("Results")
|
||||
.toolbar {
|
||||
filterMenu
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
performSearch()
|
||||
}
|
||||
}
|
||||
|
||||
var oldUI: some View {
|
||||
VStack {
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.padding()
|
||||
} else if searchResults.isEmpty {
|
||||
Text("No results found")
|
||||
.foregroundColor(.secondary)
|
||||
.padding()
|
||||
} else {
|
||||
List {
|
||||
ForEach(filteredResults) { result in
|
||||
NavigationLink(destination: MediaView(module: module!, item: result)) {
|
||||
HStack {
|
||||
KFImage(URL(string: result.imageUrl))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 100, height: 150)
|
||||
.clipped()
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(result.name)
|
||||
.font(.system(size: 16))
|
||||
.padding(.leading, 10)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Results")
|
||||
.toolbar {
|
||||
filterMenu
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.onAppear {
|
||||
performSearch()
|
||||
}
|
||||
}
|
||||
|
||||
var filterMenu: some View {
|
||||
Menu {
|
||||
ForEach([FilterType.all], id: \.self) { filter in
|
||||
Button(action: {
|
||||
self.filter = filter
|
||||
performSearch()
|
||||
}) {
|
||||
Label(filter.rawValue, systemImage: self.filter == filter ? "checkmark" : "")
|
||||
}
|
||||
}
|
||||
Menu("Audio") {
|
||||
ForEach([FilterType.dub, FilterType.sub], id: \.self) { filter in
|
||||
Button(action: {
|
||||
self.filter = filter
|
||||
performSearch()
|
||||
}) {
|
||||
Label(filter.rawValue, systemImage: self.filter == filter ? "checkmark" : "")
|
||||
}
|
||||
}
|
||||
}
|
||||
Menu("Format") {
|
||||
ForEach([FilterType.ova, FilterType.ona, FilterType.movie], id: \.self) { filter in
|
||||
Button(action: {
|
||||
self.filter = filter
|
||||
performSearch()
|
||||
}) {
|
||||
Label(filter.rawValue, systemImage: self.filter == filter ? "checkmark" : "")
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Filter", systemImage: filter == .all ? "line.horizontal.3.decrease.circle" : "line.horizontal.3.decrease.circle.fill")
|
||||
}
|
||||
}
|
||||
|
||||
var filteredResults: [ItemResult] {
|
||||
switch filter {
|
||||
case .all:
|
||||
return searchResults
|
||||
case .dub:
|
||||
return searchResults.filter { $0.name.contains("Dub") || $0.name.contains("ITA") }
|
||||
case .sub:
|
||||
return searchResults.filter { !$0.name.contains("Dub") && !$0.name.contains("ITA") }
|
||||
case .ova, .ona:
|
||||
return searchResults.filter { $0.name.contains(filter.rawValue) }
|
||||
case .movie:
|
||||
return searchResults.filter { $0.name.contains("Movie") || $0.name.contains("Film") }
|
||||
}
|
||||
}
|
||||
|
||||
func performSearch() {
|
||||
guard let module = module, !searchText.isEmpty else { return }
|
||||
|
||||
let encodedSearchText = searchText.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? searchText
|
||||
let parameter = module.module[0].search.parameter
|
||||
let urlString: String
|
||||
|
||||
if parameter == "blank" {
|
||||
urlString = "\(module.module[0].search.url)\(encodedSearchText)"
|
||||
} else {
|
||||
urlString = "\(module.module[0].search.url)?\(parameter)=\(encodedSearchText)"
|
||||
}
|
||||
|
||||
guard let url = URL(string: urlString) else { return }
|
||||
|
||||
URLSession.custom.dataTask(with: url) { data, _, error in
|
||||
defer { isLoading = false }
|
||||
guard let data = data, error == nil else { return }
|
||||
|
||||
do {
|
||||
let html = String(data: data, encoding: .utf8) ?? ""
|
||||
let document = try SwiftSoup.parse(html)
|
||||
let elements = try document.select(module.module[0].search.documentSelector)
|
||||
|
||||
var results: [ItemResult] = []
|
||||
for element in elements {
|
||||
let title = try element.select(module.module[0].search.title).text()
|
||||
let href = try element.select(module.module[0].search.href).attr("href")
|
||||
var imageURL = try element.select(module.module[0].search.image.url).attr(module.module[0].search.image.attribute)
|
||||
|
||||
if imageURL.contains(",") {
|
||||
imageURL = imageURL.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.first ?? imageURL
|
||||
}
|
||||
|
||||
if !imageURL.starts(with: "http") {
|
||||
imageURL = "\(module.module[0].details.baseURL.hasSuffix("/") ? module.module[0].details.baseURL : "\(module.module[0].details.baseURL)/")\(imageURL.hasPrefix("/") ? String(imageURL.dropFirst()) : imageURL)"
|
||||
}
|
||||
|
||||
imageURL = imageURL.replacingOccurrences(of: " ", with: "%20")
|
||||
|
||||
// If imageURL is not available or is the same as the baseURL, use a default image
|
||||
if imageURL.isEmpty || imageURL == module.module[0].details.baseURL + "/" {
|
||||
imageURL = "https://s4.anilist.co/file/anilistcdn/character/large/default.jpg"
|
||||
}
|
||||
|
||||
let result = ItemResult(name: title, imageUrl: imageURL, href: href)
|
||||
results.append(result)
|
||||
}
|
||||
|
||||
// Filter out non-searchable modules
|
||||
if module.module[0].search.searchable == false {
|
||||
results = results.filter { $0.name.lowercased().contains(searchText.lowercased()) }
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.searchResults = results
|
||||
}
|
||||
} catch {
|
||||
print("Error parsing HTML: \(error)")
|
||||
Logger.shared.log("Error parsing HTML: \(error)")
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
//
|
||||
// SearchView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ItemResult: Identifiable {
|
||||
let id = UUID()
|
||||
let name: String
|
||||
let imageUrl: String
|
||||
let href: String
|
||||
}
|
||||
|
||||
struct SearchView: View {
|
||||
@State private var searchText: String = ""
|
||||
@State private var searchResults: [ItemResult] = []
|
||||
@State private var navigateToResults: Bool = false
|
||||
@State private var selectedModule: ModuleStruct?
|
||||
@State private var showAlert = false
|
||||
@StateObject private var modulesManager = ModulesManager()
|
||||
@ObservedObject private var searchHistoryManager = HistoryManager()
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack {
|
||||
HStack {
|
||||
Menu {
|
||||
ForEach(modulesManager.modules, id: \.name) { module in
|
||||
Button(action: {
|
||||
selectedModule = module
|
||||
}) {
|
||||
Text(module.name)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label(selectedModule?.name ?? "Select Module", systemImage: "chevron.down")
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
SearchBar(text: $searchText, onSearchButtonClicked: {
|
||||
if let _ = selectedModule, !searchText.isEmpty {
|
||||
searchHistoryManager.addSearchHistory(searchText)
|
||||
navigateToResults = true
|
||||
} else {
|
||||
showAlert = true
|
||||
Logger.shared.log("No Module is selected for the search")
|
||||
}
|
||||
})
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
List {
|
||||
if !searchHistoryManager.searchHistory.isEmpty {
|
||||
Section(header: Text("Search History")) {
|
||||
ForEach(searchHistoryManager.searchHistory, id: \.self) { historyItem in
|
||||
Button(action: {
|
||||
searchText = historyItem
|
||||
if let _ = selectedModule, !searchText.isEmpty {
|
||||
navigateToResults = true
|
||||
} else {
|
||||
showAlert = true
|
||||
Logger.shared.log("No Module is selected for the search")
|
||||
}
|
||||
}) {
|
||||
Text(historyItem)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
.onDelete(perform: searchHistoryManager.deleteHistoryItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Search")
|
||||
.onSubmit(of: .search) {
|
||||
if let _ = selectedModule, !searchText.isEmpty {
|
||||
navigateToResults = true
|
||||
} else {
|
||||
showAlert = true
|
||||
Logger.shared.log("No Module is selected for the search")
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(
|
||||
destination: SearchResultsView(module: selectedModule, searchText: searchText),
|
||||
isActive: $navigateToResults,
|
||||
label: {
|
||||
EmptyView()
|
||||
}
|
||||
)
|
||||
.hidden()
|
||||
}
|
||||
.onAppear {
|
||||
modulesManager.loadModules()
|
||||
NotificationCenter.default.addObserver(forName: .moduleAdded, object: nil, queue: .main) { _ in
|
||||
modulesManager.loadModules()
|
||||
}
|
||||
NotificationCenter.default.addObserver(forName: .moduleRemoved, object: nil, queue: .main) { _ in
|
||||
modulesManager.loadModules()
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $showAlert) {
|
||||
Alert(
|
||||
title: Text("No module selected"),
|
||||
message: Text("Please select a module before searching."),
|
||||
dismissButton: .default(Text("OK"))
|
||||
)
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
}
|
||||
|
||||
struct SearchBar: View {
|
||||
@Binding var text: String
|
||||
var onSearchButtonClicked: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
TextField("Search...", text: $text, onCommit: onSearchButtonClicked)
|
||||
.padding(7)
|
||||
.padding(.horizontal, 25)
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(8)
|
||||
.overlay(
|
||||
HStack {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(.gray)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.leading, 8)
|
||||
|
||||
if !text.isEmpty {
|
||||
Button(action: {
|
||||
self.text = ""
|
||||
}) {
|
||||
Image(systemName: "multiply.circle.fill")
|
||||
.foregroundColor(.gray)
|
||||
.padding(.trailing, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.padding(.horizontal, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,303 +0,0 @@
|
|||
//
|
||||
// SettingView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var settings: Settings
|
||||
@State private var isDocumentPickerPresented = false
|
||||
@State private var showImportSuccessAlert = false
|
||||
@State private var showImportFailAlert = false
|
||||
@State private var importErrorMessage = ""
|
||||
@State private var miruDataToImport: MiruDataStruct?
|
||||
@State private var selectedModule: ModuleStruct?
|
||||
@StateObject private var libraryManager = LibraryManager.shared
|
||||
@StateObject private var modulesManager = ModulesManager()
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
Section(header: Text("Interface")) {
|
||||
ColorPicker("Accent Color", selection: $settings.accentColor)
|
||||
HStack() {
|
||||
Text("Appearance")
|
||||
Picker("Appearance", selection: $settings.selectedAppearance) {
|
||||
Text("System").tag(Appearance.system)
|
||||
Text("Light").tag(Appearance.light)
|
||||
Text("Dark").tag(Appearance.dark)
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
}
|
||||
NavigationLink(destination: SettingsIUView()) {
|
||||
Text("Interface Settings")
|
||||
}
|
||||
NavigationLink(destination: SettingsPlayerView()) {
|
||||
Text("Media Player")
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("External Features")) {
|
||||
NavigationLink(destination: SettingsModuleView()) {
|
||||
HStack {
|
||||
Image(systemName: "puzzlepiece.fill")
|
||||
Text("Modules")
|
||||
}
|
||||
}
|
||||
NavigationLink(destination: SettingsStorageView()) {
|
||||
HStack {
|
||||
Image(systemName: "externaldrive.fill")
|
||||
Text("Storage")
|
||||
}
|
||||
}
|
||||
ForEach(modulesManager.modules.filter { $0.extractor == "dub-sub" }, id: \.name) { module in
|
||||
Button(action: {
|
||||
isDocumentPickerPresented = true
|
||||
selectedModule = module
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "tray.and.arrow.down.fill")
|
||||
Text("Import Miru Bookmarks into \(module.name)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Debug")) {
|
||||
NavigationLink(destination: SettingsLogsView()) {
|
||||
HStack {
|
||||
Image(systemName: "doc.text.fill")
|
||||
Text("Logs")
|
||||
}
|
||||
}
|
||||
NavigationLink(destination: SettingsEditorView(modulesManager: ModulesManager())) {
|
||||
HStack {
|
||||
Image(systemName: "pencil.and.outline")
|
||||
Text("Modules Editor")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Info")) {
|
||||
NavigationLink(destination: AboutView()) {
|
||||
Text("About")
|
||||
}
|
||||
NavigationLink(destination: SettingsReleasesView()) {
|
||||
Text("Releases")
|
||||
}
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://github.com/cranci1/Sora") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Text("Sora github repo")
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Image(systemName: "safari")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://github.com/cranci1/Sora/issues") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Text("Report an issue")
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Image(systemName: "safari")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://discord.gg/x7hppDWFDZ") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Text("Join the Discord")
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Image(systemName: "safari")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
.sheet(isPresented: $isDocumentPickerPresented) {
|
||||
DocumentPicker(
|
||||
libraryManager: libraryManager,
|
||||
onSuccess: { miruData in
|
||||
miruDataToImport = miruData
|
||||
if let selectedModule = selectedModule {
|
||||
libraryManager.importFromMiruData(miruData, module: selectedModule)
|
||||
showImportSuccessAlert = true
|
||||
}
|
||||
},
|
||||
onFailure: { errorMessage in
|
||||
importErrorMessage = errorMessage
|
||||
showImportFailAlert = true
|
||||
}
|
||||
)
|
||||
}
|
||||
.alert("Data Imported!", isPresented: $showImportSuccessAlert) {
|
||||
Button("OK", role: .cancel) { }
|
||||
} message: {
|
||||
Text("Miru bookmarks are now imported in Sora, enjoy!")
|
||||
}
|
||||
.alert("Import Failed", isPresented: $showImportFailAlert) {
|
||||
Button("OK", role: .cancel) { }
|
||||
} message: {
|
||||
Text(importErrorMessage)
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
}
|
||||
|
||||
struct DocumentPicker: UIViewControllerRepresentable {
|
||||
var libraryManager: LibraryManager
|
||||
var onSuccess: (MiruDataStruct) -> Void
|
||||
var onFailure: (String) -> Void
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self, libraryManager: libraryManager, onSuccess: onSuccess, onFailure: onFailure)
|
||||
}
|
||||
|
||||
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
|
||||
let picker = UIDocumentPickerViewController(forOpeningContentTypes: [UTType.json])
|
||||
picker.delegate = context.coordinator
|
||||
return picker
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {}
|
||||
|
||||
class Coordinator: NSObject, UIDocumentPickerDelegate {
|
||||
var parent: DocumentPicker
|
||||
var libraryManager: LibraryManager
|
||||
var onSuccess: (MiruDataStruct) -> Void
|
||||
var onFailure: (String) -> Void
|
||||
|
||||
init(_ parent: DocumentPicker, libraryManager: LibraryManager, onSuccess: @escaping (MiruDataStruct) -> Void, onFailure: @escaping (String) -> Void) {
|
||||
self.parent = parent
|
||||
self.libraryManager = libraryManager
|
||||
self.onSuccess = onSuccess
|
||||
self.onFailure = onFailure
|
||||
}
|
||||
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
guard let selectedFileURL = urls.first else {
|
||||
let errorMessage = "No file URL selected"
|
||||
print(errorMessage)
|
||||
Logger.shared.log(errorMessage)
|
||||
onFailure(errorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
guard selectedFileURL.startAccessingSecurityScopedResource() else {
|
||||
let errorMessage = "Could not access the file"
|
||||
print(errorMessage)
|
||||
Logger.shared.log("Could not access the Miru Backup File")
|
||||
onFailure(errorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
defer {
|
||||
selectedFileURL.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: selectedFileURL)
|
||||
var miruData = try JSONDecoder().decode(MiruDataStruct.self, from: data)
|
||||
|
||||
miruData.likes = miruData.likes.map { like in
|
||||
var updatedLike = like
|
||||
updatedLike.gogoSlug = "/series/" + like.gogoSlug
|
||||
return updatedLike
|
||||
}
|
||||
|
||||
Logger.shared.log("Imported Miru data from \(selectedFileURL)")
|
||||
onSuccess(miruData)
|
||||
} catch {
|
||||
let errorMessage = "Failed to import Miru data: \(error.localizedDescription)"
|
||||
print(errorMessage)
|
||||
Logger.shared.log(errorMessage)
|
||||
onFailure(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
||||
let errorMessage = "Document picker was closed"
|
||||
print(errorMessage)
|
||||
Logger.shared.log(errorMessage)
|
||||
onFailure(errorMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Appearance: String, CaseIterable, Identifiable {
|
||||
case system, light, dark
|
||||
|
||||
var id: String { self.rawValue }
|
||||
}
|
||||
|
||||
class Settings: ObservableObject {
|
||||
@Published var accentColor: Color {
|
||||
didSet {
|
||||
saveAccentColor(accentColor)
|
||||
}
|
||||
}
|
||||
@Published var selectedAppearance: Appearance {
|
||||
didSet {
|
||||
UserDefaults.standard.set(selectedAppearance.rawValue, forKey: "selectedAppearance")
|
||||
updateAppearance()
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
if let colorData = UserDefaults.standard.data(forKey: "accentColor"),
|
||||
let uiColor = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(colorData) as? UIColor {
|
||||
self.accentColor = Color(uiColor)
|
||||
} else {
|
||||
self.accentColor = .accentColor
|
||||
}
|
||||
if let appearanceRawValue = UserDefaults.standard.string(forKey: "selectedAppearance"),
|
||||
let appearance = Appearance(rawValue: appearanceRawValue) {
|
||||
self.selectedAppearance = appearance
|
||||
} else {
|
||||
self.selectedAppearance = .system
|
||||
}
|
||||
updateAppearance()
|
||||
}
|
||||
|
||||
private func saveAccentColor(_ color: Color) {
|
||||
let uiColor = UIColor(color)
|
||||
do {
|
||||
let colorData = try NSKeyedArchiver.archivedData(withRootObject: uiColor, requiringSecureCoding: false)
|
||||
UserDefaults.standard.set(colorData, forKey: "accentColor")
|
||||
} catch {
|
||||
print("Failed to save accent color: \(error.localizedDescription)")
|
||||
Logger.shared.log("Failed to save accent color: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
func updateAppearance() {
|
||||
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }
|
||||
switch selectedAppearance {
|
||||
case .system:
|
||||
windowScene.windows.first?.overrideUserInterfaceStyle = .unspecified
|
||||
case .light:
|
||||
windowScene.windows.first?.overrideUserInterfaceStyle = .light
|
||||
case .dark:
|
||||
windowScene.windows.first?.overrideUserInterfaceStyle = .dark
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,293 +0,0 @@
|
|||
//
|
||||
// SettingsAboutView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct AboutView: View {
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(footer: Text("Sora is a free open source app, under the GPLv3.0 License. You can find the entire Sora code in the github repo.")) {
|
||||
HStack(alignment: .center, spacing: 10) {
|
||||
KFImage(URL(string: "https://raw.githubusercontent.com/cranci1/Sora/main/Sora/Assets.xcassets/AppIcon.appiconset/180.png"))
|
||||
.resizable()
|
||||
.frame(width: 80, height: 80)
|
||||
.clipShape(Circle())
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("Sora")
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
|
||||
Text("Public beta 0.1.1")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical)
|
||||
}
|
||||
|
||||
Section(header: Text("Developer")) {
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://github.com/cranci1") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
KFImage(URL(string: "https://avatars.githubusercontent.com/u/100066266?v=4"))
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("cranci1")
|
||||
.font(.headline)
|
||||
.foregroundColor(.pink)
|
||||
Text("YAY it's me")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "safari")
|
||||
.foregroundColor(.pink)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Huge thanks"), footer: Text("A huge thanks to the Miru Development team for their support and contributions to Sora. I wont ever be able to thank them enough. Thanks a lot to them and all my discord helper.")) {
|
||||
HStack {
|
||||
KFImage(URL(string: "https://storage.ko-fi.com/cdn/useruploads/e68c31f0-7e66-4d63-934a-0508ce443bc0_e71506-30ce-4a01-9ac3-892ffcd18b77.png"))
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
|
||||
Text("Miru Development Team")
|
||||
.font(.headline)
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://github.com/bshar1865") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
KFImage(URL(string: "https://avatars.githubusercontent.com/u/98615778?v=4"))
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("MA.")
|
||||
.font(.headline)
|
||||
.foregroundColor(.orange)
|
||||
Text("Discord Helper")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "safari")
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
}
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://github.com/50n50") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
KFImage(URL(string: "https://avatars.githubusercontent.com/u/80717571?v=4"))
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("50/50")
|
||||
.font(.headline)
|
||||
.foregroundColor(.mint)
|
||||
Text("Discord Helper & Designer")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "safari")
|
||||
.foregroundColor(.mint)
|
||||
}
|
||||
}
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://github.com/IBH-RAD") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
KFImage(URL(string: "https://avatars.githubusercontent.com/u/116025932?u=393be7ee3f476362b9e09d4f195ac035c5060236&v=4"))
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("IBH")
|
||||
.font(.headline)
|
||||
.foregroundColor(.purple)
|
||||
Text("Discord Helper & Bug Hunter")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "safari")
|
||||
.foregroundColor(.purple)
|
||||
}
|
||||
}
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://github.com/Seeike") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
KFImage(URL(string: "https://avatars.githubusercontent.com/u/122684677?v=4"))
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("Seiike")
|
||||
.font(.headline)
|
||||
.foregroundColor(.yellow)
|
||||
Text("Discord Helper")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "safari")
|
||||
.foregroundColor(.yellow)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Acknowledgements"), footer: Text("Thanks to the creators of this frameworks, that made Sora creation much simplier.")) {
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://github.com/scinfu/SwiftSoup") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
KFImage(URL(string: "https://raw.githubusercontent.com/scinfu/SwiftSoup/master/swiftsoup.png"))
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("SwiftSoup")
|
||||
.font(.headline)
|
||||
.foregroundColor(.red)
|
||||
Text("Web scraping")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "safari")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://github.com/onevcat/Kingfisher") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
KFImage(URL(string: "https://products.fileformat.com/image/swift/kingfisher/header-image.png"))
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("Kingfisher")
|
||||
.font(.headline)
|
||||
.foregroundColor(.blue)
|
||||
Text("Images caching and loading")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "safari")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://github.com/ipavlidakis/OpenCastSwift") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
KFImage(URL(string: "https://avatars.githubusercontent.com/u/575802?v=4"))
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("OpenCastSwift")
|
||||
.font(.headline)
|
||||
.foregroundColor(.green)
|
||||
Text("Casting support")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "safari")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
}
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://github.com/SwiftyJSON/SwiftyJSON") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
KFImage(URL(string: "https://avatars.githubusercontent.com/u/8858017?s=200&v=4"))
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("SwiftyJSON")
|
||||
.font(.headline)
|
||||
.foregroundColor(.orange)
|
||||
Text("Opencast dependency")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "safari")
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
}
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://github.com/apple/swift-protobuf") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
KFImage(URL(string: "https://avatars.githubusercontent.com/u/10639145?s=200&v=4"))
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("Swift Protobuf")
|
||||
.font(.headline)
|
||||
.foregroundColor(.purple)
|
||||
Text("Opencast dependency")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "safari")
|
||||
.foregroundColor(.purple)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("About")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
//
|
||||
// SettingsEditorView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 03/01/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsEditorView: View {
|
||||
@ObservedObject var modulesManager: ModulesManager
|
||||
@State private var jsonText: String = ""
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
TextEditor(text: $jsonText)
|
||||
.padding()
|
||||
.onAppear {
|
||||
if let data = try? JSONEncoder().encode(modulesManager.modules),
|
||||
let jsonString = String(data: data, encoding: .utf8) {
|
||||
jsonText = jsonString
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Editor")
|
||||
.navigationBarItems(trailing: Button("Save") {
|
||||
if let data = jsonText.data(using: .utf8),
|
||||
let modules = try? JSONDecoder().decode([ModuleStruct].self, from: data) {
|
||||
modulesManager.modules = modules
|
||||
modulesManager.saveModuleData()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
//
|
||||
// SettingsIUView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsIUView: View {
|
||||
@AppStorage("listSearch") private var isListSearchEnabled: Bool = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(header: Text("Search")) {
|
||||
Toggle("List Search Style", isOn: $isListSearchEnabled)
|
||||
.tint(.accentColor)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Interface Preference")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
//
|
||||
// SettingsLogsView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsLogsView: View {
|
||||
@State private var logs: String = ""
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ScrollView {
|
||||
Text(logs)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
.navigationTitle("Logs")
|
||||
.onAppear {
|
||||
logs = Logger.shared.getLogs()
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Menu {
|
||||
Button(action: {
|
||||
UIPasteboard.general.string = logs
|
||||
}) {
|
||||
Label("Copy to Clipboard", systemImage: "doc.on.doc")
|
||||
}
|
||||
Button(role: .destructive, action: {
|
||||
Logger.shared.clearLogs()
|
||||
logs = Logger.shared.getLogs()
|
||||
}) {
|
||||
Label("Clear Logs", systemImage: "trash")
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Logger {
|
||||
static let shared = Logger()
|
||||
private var logs: [(message: String, timestamp: Date)] = []
|
||||
|
||||
private init() {}
|
||||
|
||||
func log(_ message: String) {
|
||||
logs.append((message: message, timestamp: Date()))
|
||||
}
|
||||
|
||||
func getLogs() -> String {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
return logs.map { "[\(dateFormatter.string(from: $0.timestamp))] \($0.message)" }
|
||||
.joined(separator: "\n---\n")
|
||||
}
|
||||
|
||||
func clearLogs() {
|
||||
logs.removeAll()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
//
|
||||
// SettingsModuleView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ErrorMessage: Identifiable {
|
||||
var id: String { message }
|
||||
let message: String
|
||||
}
|
||||
|
||||
struct SettingsModuleView: View {
|
||||
@StateObject private var modulesManager = ModulesManager()
|
||||
@State private var showingAddModuleAlert = false
|
||||
@State private var moduleURL = ""
|
||||
@State private var errorMessage: ErrorMessage?
|
||||
@State private var previusImageURLs: [String: String] = [:]
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if modulesManager.isLoading {
|
||||
ProgressView("Loading Modules...")
|
||||
} else {
|
||||
List {
|
||||
ForEach(modulesManager.modules, id: \.name) { module in
|
||||
HStack {
|
||||
if let url = URL(string: module.iconURL) {
|
||||
if previusImageURLs[module.name] != module.iconURL {
|
||||
KFImage(url)
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.clipShape(Circle())
|
||||
.padding(.trailing, 10)
|
||||
.onAppear {
|
||||
previusImageURLs[module.name] = module.iconURL
|
||||
}
|
||||
} else {
|
||||
KFImage(url)
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.clipShape(Circle())
|
||||
.padding(.trailing, 10)
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
Text(module.name)
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
Text("Version: \(module.version)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Text("Author: \(module.author.name)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Text("Language: \(module.language)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Text(module.stream)
|
||||
.font(.caption)
|
||||
.padding(5)
|
||||
.background(Color.accentColor)
|
||||
.foregroundColor(Color.primary)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
.contextMenu {
|
||||
Button(action: {
|
||||
UIPasteboard.general.string = modulesManager.moduleURLs[module.name]
|
||||
}) {
|
||||
Label("Copy URL", systemImage: "doc.on.doc")
|
||||
}
|
||||
Button(role: .destructive, action: {
|
||||
modulesManager.deleteModule(named: module.name)
|
||||
}) {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDelete(perform: deleteModule)
|
||||
}
|
||||
.navigationBarTitle("Modules")
|
||||
.navigationBarItems(trailing: Button(action: {
|
||||
showAddModuleAlert()
|
||||
}) {
|
||||
Image(systemName: "plus")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
})
|
||||
.refreshable {
|
||||
modulesManager.refreshModules()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
modulesManager.loadModules()
|
||||
}
|
||||
.alert(item: $errorMessage) { error in
|
||||
Alert(title: Text("Error"), message: Text(error.message), dismissButton: .default(Text("OK")))
|
||||
}
|
||||
}
|
||||
|
||||
func showAddModuleAlert() {
|
||||
let alert = UIAlertController(title: "Add Module", message: "Enter the URL of the module file", preferredStyle: .alert)
|
||||
alert.addTextField { textField in
|
||||
textField.placeholder = "https://real.url/module.json"
|
||||
}
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
||||
alert.addAction(UIAlertAction(title: "Add", style: .default, handler: { _ in
|
||||
if let url = alert.textFields?.first?.text {
|
||||
modulesManager.addModule(from: url) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
errorMessage = ErrorMessage(message: error.localizedDescription)
|
||||
Logger.shared.log(error.localizedDescription.capitalized)
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootViewController = windowScene.windows.first?.rootViewController {
|
||||
rootViewController.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteModule(at offsets: IndexSet) {
|
||||
offsets.forEach { index in
|
||||
let module = modulesManager.modules[index]
|
||||
modulesManager.deleteModule(named: module.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
//
|
||||
// SettingsPlayerView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsPlayerView: View {
|
||||
@AppStorage("externalPlayer") private var externalPlayer: String = "Default"
|
||||
@AppStorage("AlwaysLandscape") private var isAlwaysLandscape = false
|
||||
@AppStorage("holdSpeedPlayer") private var holdSpeedPlayer: Double = 2.0
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(header: Text("Media Player"), footer: Text("The Force Landscape and HoldSpeed only work inside the default iOS player and Sora player.")) {
|
||||
HStack {
|
||||
Text("Media Player")
|
||||
Spacer()
|
||||
Menu(externalPlayer) {
|
||||
Button(action: {
|
||||
externalPlayer = "Default"
|
||||
}) {
|
||||
Label("Default", systemImage: externalPlayer == "Default" ? "checkmark" : "")
|
||||
}
|
||||
Button(action: {
|
||||
externalPlayer = "VLC"
|
||||
}) {
|
||||
Label("VLC", systemImage: externalPlayer == "VLC" ? "checkmark" : "")
|
||||
}
|
||||
Button(action: {
|
||||
externalPlayer = "OutPlayer"
|
||||
}) {
|
||||
Label("OutPlayer", systemImage: externalPlayer == "OutPlayer" ? "checkmark" : "")
|
||||
}
|
||||
Button(action: {
|
||||
externalPlayer = "Infuse"
|
||||
}) {
|
||||
Label("Infuse", systemImage: externalPlayer == "Infuse" ? "checkmark" : "")
|
||||
}
|
||||
Button(action: {
|
||||
externalPlayer = "nPlayer"
|
||||
}) {
|
||||
Label("nPlayer", systemImage: externalPlayer == "nPlayer" ? "checkmark" : "")
|
||||
}
|
||||
Button(action: {
|
||||
externalPlayer = "Sora"
|
||||
}) {
|
||||
Label("Sora", systemImage: externalPlayer == "Sora" ? "checkmark" : "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Hold Speed:")
|
||||
Spacer()
|
||||
Stepper(
|
||||
value: $holdSpeedPlayer,
|
||||
in: 0.25...2.0,
|
||||
step: 0.25
|
||||
) {
|
||||
Text(String(format: "%.2f", holdSpeedPlayer))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Player")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
//
|
||||
// SettingsReleasesView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 31/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsReleasesView: View {
|
||||
@State private var releases: [GitHubReleases] = []
|
||||
|
||||
var body: some View {
|
||||
List(releases, id: \.tagName) { release in
|
||||
VStack(alignment: .leading) {
|
||||
Text(release.tagName)
|
||||
.font(.system(size: 17))
|
||||
.bold()
|
||||
Text(release.body)
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
.contextMenu {
|
||||
Button(action: {
|
||||
if let url = URL(string: release.htmlUrl) {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
Text("View on GitHub")
|
||||
Image(systemName: "safari")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Releases")
|
||||
.onAppear {
|
||||
GitHubAPI.shared.fetchReleases { fetchedReleases in
|
||||
if let fetchedReleases = fetchedReleases {
|
||||
self.releases = fetchedReleases
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,269 +0,0 @@
|
|||
//
|
||||
// SettingsStorageView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 19/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsStorageView: View {
|
||||
@State private var appSize: String = "Calculating..."
|
||||
@State private var storageDetails: [(String, Double, Color)] = []
|
||||
@State private var deviceStorage: (total: Int64, used: Int64) = (0, 0)
|
||||
@State private var showingClearCacheAlert = false
|
||||
@State private var showingClearDocumentsAlert = false
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(alignment: .center) {
|
||||
Text(appSize)
|
||||
.font(.system(size: 28, weight: .bold))
|
||||
Text("of \(ByteCountFormatter.string(fromByteCount: deviceStorage.total, countStyle: .file))")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .leading) {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
.frame(height: 24)
|
||||
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: geometry.size.width * CGFloat(deviceStorage.used) / CGFloat(deviceStorage.total))
|
||||
.frame(height: 24)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
ForEach(storageDetails, id: \.0) { detail in
|
||||
RoundedRectangle(cornerRadius: 0)
|
||||
.fill(detail.2)
|
||||
.frame(width: geometry.size.width * CGFloat(detail.1 * 1024 * 1024) / CGFloat(deviceStorage.total))
|
||||
.frame(height: 24)
|
||||
}
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
}
|
||||
.frame(height: 24)
|
||||
|
||||
HStack(spacing: 16) {
|
||||
ForEach(storageDetails, id: \.0) { detail in
|
||||
HStack(spacing: 4) {
|
||||
Circle()
|
||||
.fill(detail.2)
|
||||
.frame(width: 8, height: 8)
|
||||
Text(detail.0)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
HStack(spacing: 4) {
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 8, height: 8)
|
||||
Text("System")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
Section {
|
||||
ForEach(storageDetails, id: \.0) { detail in
|
||||
HStack {
|
||||
Image(systemName: categoryIcon(for: detail.0))
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 28, height: 28)
|
||||
.background(detail.2)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(detail.0)
|
||||
Text("\(detail.1, specifier: "%.2f") MB")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Actions")) {
|
||||
Button(action: { showingClearCacheAlert = true }) {
|
||||
actionRow(
|
||||
icon: "clock.fill",
|
||||
title: "Clear Cache",
|
||||
subtitle: "Free up space used by cached items",
|
||||
iconColor: .accentColor
|
||||
)
|
||||
}
|
||||
|
||||
Button(action: { showingClearDocumentsAlert = true }) {
|
||||
actionRow(
|
||||
icon: "doc.fill",
|
||||
title: "Clear Documents",
|
||||
subtitle: "Check and remove unnecessary files",
|
||||
iconColor: .accentColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Storage")
|
||||
.onAppear {
|
||||
calculateAppSize()
|
||||
getDeviceStorage()
|
||||
}
|
||||
.alert("Clear Cache", isPresented: $showingClearCacheAlert) {
|
||||
Button("Cancel", role: .cancel) { }
|
||||
Button("Clear", role: .destructive) {
|
||||
clearCache()
|
||||
}
|
||||
} message: {
|
||||
Text("Are you sure you want to clear the cache? This action cannot be undone.")
|
||||
}
|
||||
.alert("Clear Documents", isPresented: $showingClearDocumentsAlert) {
|
||||
Button("Cancel", role: .cancel) { }
|
||||
Button("Clear", role: .destructive) {
|
||||
clearDocuments()
|
||||
}
|
||||
} message: {
|
||||
Text("Are you sure you want to clear all documents? This action cannot be undone.")
|
||||
}
|
||||
}
|
||||
|
||||
private func clearCache() {
|
||||
if let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first {
|
||||
do {
|
||||
let fileURLs = try FileManager.default.contentsOfDirectory(
|
||||
at: cacheURL,
|
||||
includingPropertiesForKeys: nil,
|
||||
options: .skipsHiddenFiles
|
||||
)
|
||||
|
||||
for fileURL in fileURLs {
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
|
||||
calculateAppSize()
|
||||
getDeviceStorage()
|
||||
Logger.shared.log("Cleared Cache")
|
||||
} catch {
|
||||
print("Error clearing cache: \(error)")
|
||||
Logger.shared.log("Error clearing cache: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func clearDocuments() {
|
||||
if let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
|
||||
do {
|
||||
let fileURLs = try FileManager.default.contentsOfDirectory(
|
||||
at: documentsURL,
|
||||
includingPropertiesForKeys: nil,
|
||||
options: .skipsHiddenFiles
|
||||
)
|
||||
|
||||
for fileURL in fileURLs {
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
|
||||
calculateAppSize()
|
||||
getDeviceStorage()
|
||||
Logger.shared.log("Cleared Documents")
|
||||
} catch {
|
||||
print("Error clearing documents: \(error)")
|
||||
Logger.shared.log("Error clearing documents: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func getDeviceStorage() {
|
||||
do {
|
||||
let fileURL = URL(fileURLWithPath: NSHomeDirectory() as String)
|
||||
let values = try fileURL.resourceValues(forKeys: [.volumeTotalCapacityKey, .volumeAvailableCapacityKey])
|
||||
|
||||
if let total = values.volumeTotalCapacity,
|
||||
let available = values.volumeAvailableCapacity {
|
||||
deviceStorage.total = Int64(total)
|
||||
deviceStorage.used = Int64(total - available)
|
||||
}
|
||||
} catch {
|
||||
print("Error getting device storage: \(error)")
|
||||
Logger.shared.log("Error getting device storage: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func getTotalAppBytes() -> Int64 {
|
||||
return Int64(totalSize() * 1024 * 1024)
|
||||
}
|
||||
|
||||
private func actionRow(icon: String, title: String, subtitle: String, iconColor: Color) -> some View {
|
||||
HStack {
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 28, height: 28)
|
||||
.background(iconColor)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(title)
|
||||
Text(subtitle)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func categoryIcon(for category: String) -> String {
|
||||
switch category {
|
||||
case "Documents":
|
||||
return "doc.fill"
|
||||
case "Cache":
|
||||
return "clock.fill"
|
||||
case "Temporary":
|
||||
return "trash.fill"
|
||||
default:
|
||||
return "questionmark"
|
||||
}
|
||||
}
|
||||
|
||||
private func calculateAppSize() {
|
||||
let cacheSize = getDirectorySize(url: FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!)
|
||||
let documentsSize = getDirectorySize(url: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!)
|
||||
let tmpSize = getDirectorySize(url: FileManager.default.temporaryDirectory)
|
||||
|
||||
let totalSize = cacheSize + documentsSize + tmpSize
|
||||
self.appSize = ByteCountFormatter.string(fromByteCount: Int64(totalSize), countStyle: .file)
|
||||
self.storageDetails = [
|
||||
("Documents", documentsSize / 1024 / 1024, .green),
|
||||
("Cache", cacheSize / 1024 / 1024, .orange),
|
||||
("Temporary", tmpSize / 1024 / 1024, .red)
|
||||
]
|
||||
}
|
||||
|
||||
private func getDirectorySize(url: URL) -> Double {
|
||||
var size: Double = 0
|
||||
if let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.fileSizeKey], options: [], errorHandler: nil) {
|
||||
for case let fileURL as URL in enumerator {
|
||||
do {
|
||||
let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
|
||||
size += Double(resourceValues.fileSize ?? 0)
|
||||
} catch {
|
||||
print("Error calculating size for file \(fileURL): \(error)")
|
||||
Logger.shared.log("Error calculating size for file \(fileURL): \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
private func totalSize() -> Double {
|
||||
return storageDetails.reduce(0) { $0 + $1.1 }
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 222 KiB |
42
ipabuild.sh
|
|
@ -1,42 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
WORKING_LOCATION="$(pwd)"
|
||||
APPLICATION_NAME=Sora
|
||||
|
||||
if [ ! -d "build" ]; then
|
||||
mkdir build
|
||||
fi
|
||||
|
||||
cd build
|
||||
|
||||
xcodebuild -project "$WORKING_LOCATION/$APPLICATION_NAME.xcodeproj" \
|
||||
-scheme "$APPLICATION_NAME" \
|
||||
-configuration Release \
|
||||
-derivedDataPath "$WORKING_LOCATION/build/DerivedDataApp" \
|
||||
-destination '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
|
||||
39
macbuild.sh
|
|
@ -1,39 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
WORKING_LOCATION="$(pwd)"
|
||||
APPLICATION_NAME=Sora
|
||||
|
||||
if [ ! -d "build" ]; then
|
||||
mkdir build
|
||||
fi
|
||||
|
||||
cd build
|
||||
|
||||
xcodebuild -project "$WORKING_LOCATION/$APPLICATION_NAME.xcodeproj" \
|
||||
-scheme "$APPLICATION_NAME" \
|
||||
-configuration Release \
|
||||
-derivedDataPath "$WORKING_LOCATION/build/DerivedDataApp" \
|
||||
-destination 'platform=macOS,variant=Mac Catalyst' \
|
||||
clean build \
|
||||
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGN_ENTITLEMENTS="" CODE_SIGNING_ALLOWED="NO"
|
||||
|
||||
DD_APP_PATH="$WORKING_LOCATION/build/DerivedDataApp/Build/Products/Release-maccatalyst/$APPLICATION_NAME.app"
|
||||
TARGET_APP="$WORKING_LOCATION/build/$APPLICATION_NAME.app"
|
||||
|
||||
if [ -e "$TARGET_APP" ]; then
|
||||
rm -rf "$TARGET_APP"
|
||||
fi
|
||||
|
||||
cp -r "$DD_APP_PATH" "$TARGET_APP"
|
||||
|
||||
codesign --remove "$TARGET_APP"
|
||||
if [ -e "$TARGET_APP/_CodeSignature" ]; then
|
||||
rm -rf "$TARGET_APP/_CodeSignature"
|
||||
fi
|
||||
|
||||
zip -vr "$APPLICATION_NAME-catalyst.zip" "$APPLICATION_NAME.app"
|
||||
rm -rf "$APPLICATION_NAME.app"
|
||||