Initial commit
@ -14,6 +14,33 @@
|
||||
628A47742E5A91DA0099CAA0 /* WhiteNightsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A47732E5A91DA0099CAA0 /* WhiteNightsTests.swift */; };
|
||||
628A477E2E5A91DA0099CAA0 /* WhiteNightsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A477D2E5A91DA0099CAA0 /* WhiteNightsUITests.swift */; };
|
||||
628A47802E5A91DA0099CAA0 /* WhiteNightsUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A477F2E5A91DA0099CAA0 /* WhiteNightsUITestsLaunchTests.swift */; };
|
||||
628A478D2E5A922A0099CAA0 /* WeatherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A478C2E5A922A0099CAA0 /* WeatherView.swift */; };
|
||||
628A47902E5A92560099CAA0 /* RouteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A478F2E5A92560099CAA0 /* RouteView.swift */; };
|
||||
628A47922E5A926A0099CAA0 /* SightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A47912E5A926A0099CAA0 /* SightView.swift */; };
|
||||
628A47962E5A92CF0099CAA0 /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A47952E5A92CF0099CAA0 /* Color+Extensions.swift */; };
|
||||
628A47982E5A92FC0099CAA0 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A47972E5A92FC0099CAA0 /* View+Extensions.swift */; };
|
||||
628A479B2E5A935E0099CAA0 /* WeaherModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A479A2E5A935E0099CAA0 /* WeaherModel.swift */; };
|
||||
628A479D2E5A94D30099CAA0 /* SightModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A479C2E5A94D30099CAA0 /* SightModel.swift */; };
|
||||
628A479F2E5A94EB0099CAA0 /* Article.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A479E2E5A94EB0099CAA0 /* Article.swift */; };
|
||||
628A47A12E5A94FA0099CAA0 /* ArticleMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A47A02E5A94FA0099CAA0 /* ArticleMedia.swift */; };
|
||||
628A47A32E5A951B0099CAA0 /* SightViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A47A22E5A951B0099CAA0 /* SightViewModel.swift */; };
|
||||
628A47A52E5A957A0099CAA0 /* MarqueeText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A47A42E5A957A0099CAA0 /* MarqueeText.swift */; };
|
||||
628A47A72E5A95F60099CAA0 /* Station.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A47A62E5A95F60099CAA0 /* Station.swift */; };
|
||||
628A47A92E5A961D0099CAA0 /* View+CornerRadius.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A47A82E5A961D0099CAA0 /* View+CornerRadius.swift */; };
|
||||
628A47AB2E5A96480099CAA0 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A47AA2E5A96480099CAA0 /* AppState.swift */; };
|
||||
628A47AD2E5A965D0099CAA0 /* BottomMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A47AC2E5A965D0099CAA0 /* BottomMenu.swift */; };
|
||||
628A47B02E5A96AC0099CAA0 /* SDWebImageSVGCoder in Frameworks */ = {isa = PBXBuildFile; productRef = 628A47AF2E5A96AC0099CAA0 /* SDWebImageSVGCoder */; };
|
||||
628A47B32E5A96B80099CAA0 /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 628A47B22E5A96B80099CAA0 /* SDWebImageSwiftUI */; };
|
||||
628A47B62E5A96D00099CAA0 /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = 628A47B52E5A96D00099CAA0 /* SVGKit */; };
|
||||
628A47B82E5A96D00099CAA0 /* SVGKitSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 628A47B72E5A96D00099CAA0 /* SVGKitSwift */; };
|
||||
628A47BB2E5A96E80099CAA0 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = 628A47BA2E5A96E80099CAA0 /* Nuke */; };
|
||||
628A47BD2E5A96E80099CAA0 /* NukeExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 628A47BC2E5A96E80099CAA0 /* NukeExtensions */; };
|
||||
628A47BF2E5A96E80099CAA0 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 628A47BE2E5A96E80099CAA0 /* NukeUI */; };
|
||||
628A47C12E5A96E80099CAA0 /* NukeVideo in Frameworks */ = {isa = PBXBuildFile; productRef = 628A47C02E5A96E80099CAA0 /* NukeVideo */; };
|
||||
628A47C32E5A98980099CAA0 /* Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A47C22E5A98980099CAA0 /* Route.swift */; };
|
||||
628A47C72E5A99A20099CAA0 /* VisualEffectBlur.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A47C62E5A99A20099CAA0 /* VisualEffectBlur.swift */; };
|
||||
628A47C92E5A9A970099CAA0 /* StopModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A47C82E5A9A970099CAA0 /* StopModels.swift */; };
|
||||
628A47CB2E5A9BD60099CAA0 /* RouteSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A47CA2E5A9BD60099CAA0 /* RouteSelectionView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@ -45,6 +72,25 @@
|
||||
628A47792E5A91DA0099CAA0 /* WhiteNightsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WhiteNightsUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
628A477D2E5A91DA0099CAA0 /* WhiteNightsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiteNightsUITests.swift; sourceTree = "<group>"; };
|
||||
628A477F2E5A91DA0099CAA0 /* WhiteNightsUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiteNightsUITestsLaunchTests.swift; sourceTree = "<group>"; };
|
||||
628A478C2E5A922A0099CAA0 /* WeatherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherView.swift; sourceTree = "<group>"; };
|
||||
628A478F2E5A92560099CAA0 /* RouteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteView.swift; sourceTree = "<group>"; };
|
||||
628A47912E5A926A0099CAA0 /* SightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SightView.swift; sourceTree = "<group>"; };
|
||||
628A47952E5A92CF0099CAA0 /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = "<group>"; };
|
||||
628A47972E5A92FC0099CAA0 /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
|
||||
628A479A2E5A935E0099CAA0 /* WeaherModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeaherModel.swift; sourceTree = "<group>"; };
|
||||
628A479C2E5A94D30099CAA0 /* SightModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SightModel.swift; sourceTree = "<group>"; };
|
||||
628A479E2E5A94EB0099CAA0 /* Article.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Article.swift; sourceTree = "<group>"; };
|
||||
628A47A02E5A94FA0099CAA0 /* ArticleMedia.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleMedia.swift; sourceTree = "<group>"; };
|
||||
628A47A22E5A951B0099CAA0 /* SightViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SightViewModel.swift; sourceTree = "<group>"; };
|
||||
628A47A42E5A957A0099CAA0 /* MarqueeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarqueeText.swift; sourceTree = "<group>"; };
|
||||
628A47A62E5A95F60099CAA0 /* Station.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Station.swift; sourceTree = "<group>"; };
|
||||
628A47A82E5A961D0099CAA0 /* View+CornerRadius.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+CornerRadius.swift"; sourceTree = "<group>"; };
|
||||
628A47AA2E5A96480099CAA0 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
|
||||
628A47AC2E5A965D0099CAA0 /* BottomMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomMenu.swift; sourceTree = "<group>"; };
|
||||
628A47C22E5A98980099CAA0 /* Route.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Route.swift; sourceTree = "<group>"; };
|
||||
628A47C62E5A99A20099CAA0 /* VisualEffectBlur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectBlur.swift; sourceTree = "<group>"; };
|
||||
628A47C82E5A9A970099CAA0 /* StopModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopModels.swift; sourceTree = "<group>"; };
|
||||
628A47CA2E5A9BD60099CAA0 /* RouteSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteSelectionView.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -52,6 +98,14 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
628A47B82E5A96D00099CAA0 /* SVGKitSwift in Frameworks */,
|
||||
628A47BF2E5A96E80099CAA0 /* NukeUI in Frameworks */,
|
||||
628A47B62E5A96D00099CAA0 /* SVGKit in Frameworks */,
|
||||
628A47C12E5A96E80099CAA0 /* NukeVideo in Frameworks */,
|
||||
628A47B32E5A96B80099CAA0 /* SDWebImageSwiftUI in Frameworks */,
|
||||
628A47B02E5A96AC0099CAA0 /* SDWebImageSVGCoder in Frameworks */,
|
||||
628A47BB2E5A96E80099CAA0 /* Nuke in Frameworks */,
|
||||
628A47BD2E5A96E80099CAA0 /* NukeExtensions in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -95,11 +149,15 @@
|
||||
628A47602E5A91D80099CAA0 /* WhiteNights */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
628A47992E5A934A0099CAA0 /* Models */,
|
||||
628A47942E5A92BE0099CAA0 /* Utils */,
|
||||
628A478E2E5A923E0099CAA0 /* Widgets */,
|
||||
628A47612E5A91D80099CAA0 /* WhiteNightsApp.swift */,
|
||||
628A47632E5A91D80099CAA0 /* ContentView.swift */,
|
||||
628A47652E5A91DA0099CAA0 /* Assets.xcassets */,
|
||||
628A47672E5A91DA0099CAA0 /* WhiteNights.entitlements */,
|
||||
628A47682E5A91DA0099CAA0 /* Preview Content */,
|
||||
628A47AA2E5A96480099CAA0 /* AppState.swift */,
|
||||
);
|
||||
path = WhiteNights;
|
||||
sourceTree = "<group>";
|
||||
@ -129,6 +187,45 @@
|
||||
path = WhiteNightsUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
628A478E2E5A923E0099CAA0 /* Widgets */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
628A478C2E5A922A0099CAA0 /* WeatherView.swift */,
|
||||
628A478F2E5A92560099CAA0 /* RouteView.swift */,
|
||||
628A47912E5A926A0099CAA0 /* SightView.swift */,
|
||||
628A47A22E5A951B0099CAA0 /* SightViewModel.swift */,
|
||||
628A47AC2E5A965D0099CAA0 /* BottomMenu.swift */,
|
||||
628A47C62E5A99A20099CAA0 /* VisualEffectBlur.swift */,
|
||||
628A47CA2E5A9BD60099CAA0 /* RouteSelectionView.swift */,
|
||||
);
|
||||
path = Widgets;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
628A47942E5A92BE0099CAA0 /* Utils */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
628A47952E5A92CF0099CAA0 /* Color+Extensions.swift */,
|
||||
628A47972E5A92FC0099CAA0 /* View+Extensions.swift */,
|
||||
628A47A42E5A957A0099CAA0 /* MarqueeText.swift */,
|
||||
628A47A82E5A961D0099CAA0 /* View+CornerRadius.swift */,
|
||||
);
|
||||
path = Utils;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
628A47992E5A934A0099CAA0 /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
628A479A2E5A935E0099CAA0 /* WeaherModel.swift */,
|
||||
628A479C2E5A94D30099CAA0 /* SightModel.swift */,
|
||||
628A479E2E5A94EB0099CAA0 /* Article.swift */,
|
||||
628A47A02E5A94FA0099CAA0 /* ArticleMedia.swift */,
|
||||
628A47A62E5A95F60099CAA0 /* Station.swift */,
|
||||
628A47C22E5A98980099CAA0 /* Route.swift */,
|
||||
628A47C82E5A9A970099CAA0 /* StopModels.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@ -145,6 +242,16 @@
|
||||
dependencies = (
|
||||
);
|
||||
name = WhiteNights;
|
||||
packageProductDependencies = (
|
||||
628A47AF2E5A96AC0099CAA0 /* SDWebImageSVGCoder */,
|
||||
628A47B22E5A96B80099CAA0 /* SDWebImageSwiftUI */,
|
||||
628A47B52E5A96D00099CAA0 /* SVGKit */,
|
||||
628A47B72E5A96D00099CAA0 /* SVGKitSwift */,
|
||||
628A47BA2E5A96E80099CAA0 /* Nuke */,
|
||||
628A47BC2E5A96E80099CAA0 /* NukeExtensions */,
|
||||
628A47BE2E5A96E80099CAA0 /* NukeUI */,
|
||||
628A47C02E5A96E80099CAA0 /* NukeVideo */,
|
||||
);
|
||||
productName = WhiteNights;
|
||||
productReference = 628A475E2E5A91D80099CAA0 /* WhiteNights.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
@ -217,6 +324,12 @@
|
||||
Base,
|
||||
);
|
||||
mainGroup = 628A47552E5A91D80099CAA0;
|
||||
packageReferences = (
|
||||
628A47AE2E5A96AC0099CAA0 /* XCRemoteSwiftPackageReference "SDWebImageSVGCoder" */,
|
||||
628A47B12E5A96B80099CAA0 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */,
|
||||
628A47B42E5A96D00099CAA0 /* XCRemoteSwiftPackageReference "SVGKit" */,
|
||||
628A47B92E5A96E80099CAA0 /* XCRemoteSwiftPackageReference "Nuke" */,
|
||||
);
|
||||
productRefGroup = 628A475F2E5A91D80099CAA0 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
@ -259,8 +372,27 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
628A47902E5A92560099CAA0 /* RouteView.swift in Sources */,
|
||||
628A479B2E5A935E0099CAA0 /* WeaherModel.swift in Sources */,
|
||||
628A479D2E5A94D30099CAA0 /* SightModel.swift in Sources */,
|
||||
628A478D2E5A922A0099CAA0 /* WeatherView.swift in Sources */,
|
||||
628A47C72E5A99A20099CAA0 /* VisualEffectBlur.swift in Sources */,
|
||||
628A479F2E5A94EB0099CAA0 /* Article.swift in Sources */,
|
||||
628A47922E5A926A0099CAA0 /* SightView.swift in Sources */,
|
||||
628A47A72E5A95F60099CAA0 /* Station.swift in Sources */,
|
||||
628A47A32E5A951B0099CAA0 /* SightViewModel.swift in Sources */,
|
||||
628A47C92E5A9A970099CAA0 /* StopModels.swift in Sources */,
|
||||
628A47982E5A92FC0099CAA0 /* View+Extensions.swift in Sources */,
|
||||
628A47C32E5A98980099CAA0 /* Route.swift in Sources */,
|
||||
628A47A92E5A961D0099CAA0 /* View+CornerRadius.swift in Sources */,
|
||||
628A47CB2E5A9BD60099CAA0 /* RouteSelectionView.swift in Sources */,
|
||||
628A47642E5A91D80099CAA0 /* ContentView.swift in Sources */,
|
||||
628A47AB2E5A96480099CAA0 /* AppState.swift in Sources */,
|
||||
628A47622E5A91D80099CAA0 /* WhiteNightsApp.swift in Sources */,
|
||||
628A47AD2E5A965D0099CAA0 /* BottomMenu.swift in Sources */,
|
||||
628A47962E5A92CF0099CAA0 /* Color+Extensions.swift in Sources */,
|
||||
628A47A12E5A94FA0099CAA0 /* ArticleMedia.swift in Sources */,
|
||||
628A47A52E5A957A0099CAA0 /* MarqueeText.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -617,6 +749,84 @@
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
628A47AE2E5A96AC0099CAA0 /* XCRemoteSwiftPackageReference "SDWebImageSVGCoder" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/SDWebImage/SDWebImageSVGCoder.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.8.0;
|
||||
};
|
||||
};
|
||||
628A47B12E5A96B80099CAA0 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/SDWebImage/SDWebImageSwiftUI.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 3.1.3;
|
||||
};
|
||||
};
|
||||
628A47B42E5A96D00099CAA0 /* XCRemoteSwiftPackageReference "SVGKit" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/SVGKit/SVGKit.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 3.0.0;
|
||||
};
|
||||
};
|
||||
628A47B92E5A96E80099CAA0 /* XCRemoteSwiftPackageReference "Nuke" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/kean/Nuke.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 12.8.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
628A47AF2E5A96AC0099CAA0 /* SDWebImageSVGCoder */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 628A47AE2E5A96AC0099CAA0 /* XCRemoteSwiftPackageReference "SDWebImageSVGCoder" */;
|
||||
productName = SDWebImageSVGCoder;
|
||||
};
|
||||
628A47B22E5A96B80099CAA0 /* SDWebImageSwiftUI */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 628A47B12E5A96B80099CAA0 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */;
|
||||
productName = SDWebImageSwiftUI;
|
||||
};
|
||||
628A47B52E5A96D00099CAA0 /* SVGKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 628A47B42E5A96D00099CAA0 /* XCRemoteSwiftPackageReference "SVGKit" */;
|
||||
productName = SVGKit;
|
||||
};
|
||||
628A47B72E5A96D00099CAA0 /* SVGKitSwift */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 628A47B42E5A96D00099CAA0 /* XCRemoteSwiftPackageReference "SVGKit" */;
|
||||
productName = SVGKitSwift;
|
||||
};
|
||||
628A47BA2E5A96E80099CAA0 /* Nuke */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 628A47B92E5A96E80099CAA0 /* XCRemoteSwiftPackageReference "Nuke" */;
|
||||
productName = Nuke;
|
||||
};
|
||||
628A47BC2E5A96E80099CAA0 /* NukeExtensions */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 628A47B92E5A96E80099CAA0 /* XCRemoteSwiftPackageReference "Nuke" */;
|
||||
productName = NukeExtensions;
|
||||
};
|
||||
628A47BE2E5A96E80099CAA0 /* NukeUI */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 628A47B92E5A96E80099CAA0 /* XCRemoteSwiftPackageReference "Nuke" */;
|
||||
productName = NukeUI;
|
||||
};
|
||||
628A47C02E5A96E80099CAA0 /* NukeVideo */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 628A47B92E5A96E80099CAA0 /* XCRemoteSwiftPackageReference "Nuke" */;
|
||||
productName = NukeVideo;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 628A47562E5A91D80099CAA0 /* Project object */;
|
||||
}
|
||||
|
@ -0,0 +1,68 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "cocoalumberjack",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git",
|
||||
"state" : {
|
||||
"revision" : "4b8714a7fb84d42393314ce897127b3939885ec3",
|
||||
"version" : "3.8.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "nuke",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kean/Nuke.git",
|
||||
"state" : {
|
||||
"revision" : "0ead44350d2737db384908569c012fe67c421e4d",
|
||||
"version" : "12.8.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sdwebimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImage.git",
|
||||
"state" : {
|
||||
"revision" : "b62cb63bf4ed1f04c961a56c9c6c9d5ab8524ec6",
|
||||
"version" : "5.21.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sdwebimagesvgcoder",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImageSVGCoder.git",
|
||||
"state" : {
|
||||
"revision" : "85b5d58ad02c207c496fa34426dc6560d6ae32f0",
|
||||
"version" : "1.8.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sdwebimageswiftui",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImageSwiftUI.git",
|
||||
"state" : {
|
||||
"revision" : "451c6dfd5ecec2cf626d1d9ca81c2d4a60355172",
|
||||
"version" : "3.1.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "svgkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SVGKit/SVGKit.git",
|
||||
"state" : {
|
||||
"revision" : "58152b9f7c85eab239160b36ffdfd364aa43d666",
|
||||
"version" : "3.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-log",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-log",
|
||||
"state" : {
|
||||
"revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2",
|
||||
"version" : "1.6.4"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Bucket
|
||||
uuid = "1CDA7EE0-204E-40F9-BC57-F6B8A011D4CC"
|
||||
type = "1"
|
||||
version = "2.0">
|
||||
</Bucket>
|
72
WhiteNights/AppState.swift
Normal file
@ -0,0 +1,72 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
class AppState: ObservableObject {
|
||||
@Published var selectedRoute: Route? = nil {
|
||||
didSet {
|
||||
if let routeId = selectedRoute?.id {
|
||||
Task {
|
||||
await fetchSights(for: routeId)
|
||||
}
|
||||
} else {
|
||||
self.sights = []
|
||||
self.sightId = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published var allRoutes: [Route] = []
|
||||
@Published var sightId: Int? = nil
|
||||
@Published var sights: [SightModel] = [] // <- все достопримечательности маршрута
|
||||
|
||||
// MARK: - Fetch Sights
|
||||
private func fetchSights(for routeId: Int) async {
|
||||
let urlString = "https://white-nights.krbl.ru/services/content/route/\(routeId)/sight"
|
||||
guard let url = URL(string: urlString) else { return }
|
||||
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
let decodedSights = try JSONDecoder().decode([SightModel].self, from: data)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.sights = decodedSights
|
||||
self.sightId = decodedSights.first?.id
|
||||
}
|
||||
|
||||
// 🔹 Предзагрузка миниатюр
|
||||
preloadThumbnails(for: decodedSights)
|
||||
|
||||
} catch {
|
||||
print("Ошибка загрузки достопримечательностей: \(error)")
|
||||
DispatchQueue.main.async {
|
||||
self.sights = []
|
||||
self.sightId = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preload thumbnails into URLCache
|
||||
private func preloadThumbnails(for sights: [SightModel]) {
|
||||
let session = URLSession.shared
|
||||
for sight in sights {
|
||||
guard let url = URL(string: "https://white-nights.krbl.ru/services/content/media/\(sight.thumbnail)/download") else { continue }
|
||||
let request = URLRequest(url: url)
|
||||
|
||||
// Если уже в кэше, пропускаем
|
||||
if URLCache.shared.cachedResponse(for: request) != nil { continue }
|
||||
|
||||
let task = session.dataTask(with: request) { data, response, _ in
|
||||
guard let data = data, let response = response else { return }
|
||||
let cachedResponse = CachedURLResponse(response: response, data: data)
|
||||
URLCache.shared.storeCachedResponse(cachedResponse, for: request)
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
// Optional computed property
|
||||
var routeId: Int? {
|
||||
selectedRoute?.id
|
||||
}
|
||||
}
|
21
WhiteNights/Assets.xcassets/GAT_Icon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "GAT_Icon.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
7
WhiteNights/Assets.xcassets/GAT_Icon.imageset/GAT_Icon.svg
vendored
Normal file
After Width: | Height: | Size: 18 KiB |
21
WhiteNights/Assets.xcassets/cond_cloudy.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "cond_cloudy.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
3
WhiteNights/Assets.xcassets/cond_cloudy.imageset/cond_cloudy.svg
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M47.15 14.23C46.84 14.23 46.53 14.24 46.23 14.26C44.81 14.34 43.42 13.73 42.63 12.54C39.6 7.98 34.51 5 28.75 5C21.58 5 15.45 9.63 13.02 16.15C12.62 17.24 11.72 18.08 10.61 18.44C4.47 20.42 0 26.35 0 33.36C0 42 6.78 49 15.15 49H47.15C56.46 49 64 41.22 64 31.61C64 22.01 56.46 14.23 47.15 14.23Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 422 B |
21
WhiteNights/Assets.xcassets/cond_partycloud.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "cond_partlycloudy.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
4
WhiteNights/Assets.xcassets/cond_partycloud.imageset/cond_partlycloudy.svg
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M49.0191 29.96C57.2891 29.96 63.9991 23.25 63.9991 14.98C63.9991 6.71 57.2891 0 49.0191 0C40.7491 0 34.0391 6.71 34.0391 14.98C34.0291 23.26 40.7391 29.96 49.0191 29.96Z" fill="#FCD500"/>
|
||||
<path d="M43.4668 17.3834C43.1948 17.3834 42.9128 17.3933 42.6408 17.4033C41.2708 17.4728 39.9713 16.8073 39.1957 15.6948C36.3852 11.6422 31.7514 9 26.5032 9C19.9655 9 14.3748 13.1122 12.078 18.933C11.6348 20.0554 10.7181 20.8997 9.56975 21.2871C4.00922 23.1545 0 28.4984 0 34.7859C0 42.633 6.25559 49 13.9718 49H43.4668C52.0393 49 59 41.9277 59 33.1967C59 24.4656 52.0493 17.3834 43.4668 17.3834Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 714 B |
21
WhiteNights/Assets.xcassets/cond_rainy.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "cond_rainy.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
7
WhiteNights/Assets.xcassets/cond_rainy.imageset/cond_rainy.svg
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M47.15 11.22C46.84 11.22 46.53 11.23 46.23 11.25C44.81 11.33 43.42 10.72 42.63 9.53C39.59 4.98 34.51 2 28.75 2C21.58 2 15.45 6.63 13.02 13.15C12.62 14.24 11.72 15.08 10.61 15.44C4.47 17.41 0 23.35 0 30.35C0 38.99 6.78 45.99 15.15 45.99H47.15C56.45 45.99 64 38.21 64 28.6C64 19 56.46 11.22 47.15 11.22Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.6908 48.5015C18.4079 48.9162 18.6532 49.8337 18.2385 50.5509L14.4685 57.0709C14.0538 57.7881 13.1362 58.0333 12.4191 57.6186C11.7019 57.2039 11.4567 56.2864 11.8714 55.5692L15.6414 49.0492C16.0561 48.332 16.9736 48.0868 17.6908 48.5015Z" fill="#00B1FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.8607 53.2215C26.5779 53.6362 26.8231 54.5537 26.4084 55.2709L22.6384 61.7909C22.2237 62.508 21.3062 62.7533 20.589 62.3386C19.8718 61.9239 19.6266 61.0063 20.0413 60.2892L23.8113 53.7692C24.226 53.052 25.1435 52.8068 25.8607 53.2215Z" fill="#00B1FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.1988 48.9706C39.9165 49.3845 40.1627 50.3017 39.7489 51.0194L35.9889 57.5394C35.575 58.257 34.6578 58.5033 33.9401 58.0894C33.2225 57.6756 32.9762 56.7583 33.39 56.0407L37.1501 49.5207C37.5639 48.803 38.4812 48.5568 39.1988 48.9706Z" fill="#00B1FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M47.3687 53.6906C48.0864 54.1044 48.3326 55.0217 47.9188 55.7394L44.1588 62.2593C43.7449 62.977 42.8277 63.2233 42.11 62.8094C41.3924 62.3955 41.1461 61.4783 41.56 60.7606L45.32 54.2406C45.7338 53.523 46.6511 53.2767 47.3687 53.6906Z" fill="#00B1FF"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
21
WhiteNights/Assets.xcassets/cond_snow.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "cond_snow.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
11
WhiteNights/Assets.xcassets/cond_snow.imageset/cond_snow.svg
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.95 0.189941C33.0546 0.189941 33.95 1.08537 33.95 2.18994V61.8699C33.95 62.9745 33.0546 63.8699 31.95 63.8699C30.8454 63.8699 29.95 62.9745 29.95 61.8699V2.18994C29.95 1.08537 30.8454 0.189941 31.95 0.189941Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.4458 4.92575C21.2268 4.1447 22.4932 4.1447 23.2742 4.92575L32.0107 13.6622L40.6265 5.05505C41.4079 4.27439 42.6742 4.27502 43.4549 5.05646C44.2356 5.8379 44.2349 7.10423 43.4535 7.88488L32.0093 19.3177L20.4458 7.75418C19.6647 6.97313 19.6647 5.7068 20.4458 4.92575Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.89 45.0416L43.4542 56.6058C44.2352 57.3869 44.2352 58.6532 43.4542 59.4343C42.6732 60.2153 41.4068 60.2153 40.6258 59.4343L31.89 50.6985L23.2742 59.3143C22.4932 60.0953 21.2268 60.0953 20.4458 59.3143C19.6647 58.5332 19.6647 57.2669 20.4458 56.4858L31.89 45.0416Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.36791 16.11C4.92015 15.1534 6.14331 14.8256 7.09992 15.3779L58.7899 45.2179C59.7465 45.7701 60.0743 46.9933 59.5221 47.9499C58.9699 48.9065 57.7467 49.2343 56.7901 48.6821L5.10009 18.8421C4.14347 18.2898 3.81567 17.0666 4.36791 16.11Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.3311 8.57849C15.3979 8.29194 16.495 8.92443 16.7815 9.99118L20.9809 25.6248L5.17678 29.8521C4.10972 30.1375 3.01332 29.5039 2.7279 28.4368C2.44248 27.3698 3.07613 26.2734 4.14318 25.9879L16.0791 22.7953L12.9185 11.0289C12.6319 9.9621 13.2644 8.86503 14.3311 8.57849Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6571 8.51798C50.7242 8.80359 51.3576 9.9001 51.072 10.9671L47.8787 22.8968L59.6488 26.0584C60.7156 26.345 61.3481 27.442 61.0615 28.5088C60.775 29.5756 59.6779 30.208 58.6112 29.9215L42.9813 25.7231L47.208 9.93281C47.4936 8.86581 48.5901 8.23236 49.6571 8.51798Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.56812 35.7025C2.85394 34.6355 3.95058 34.0023 5.01753 34.2881L20.65 38.4758L16.4117 54.2781C16.1256 55.3449 15.0288 55.9778 13.9619 55.6917C12.895 55.4056 12.2621 54.3087 12.5483 53.2419L15.75 41.3042L3.98249 38.1519C2.91554 37.866 2.28231 36.7694 2.56812 35.7025Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M61.4318 35.7619C61.7179 36.8288 61.085 37.9256 60.0181 38.2118L48.0793 41.4138L51.2319 53.1825C51.5177 54.2495 50.8845 55.3461 49.8176 55.6319C48.7506 55.9177 47.654 55.2845 47.3681 54.2176L43.1808 38.5863L58.9819 34.3483C60.0488 34.0622 61.1456 34.6951 61.4318 35.7619Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M59.5221 16.11C60.0743 17.0666 59.7465 18.2898 58.7899 18.8421L7.09992 48.6821C6.14331 49.2343 4.92015 48.9065 4.36791 47.9499C3.81567 46.9933 4.14347 45.7701 5.10009 45.2179L56.7901 15.3779C57.7467 14.8256 58.9699 15.1534 59.5221 16.11Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
21
WhiteNights/Assets.xcassets/cond_snowy.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "cond_snowy.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
11
WhiteNights/Assets.xcassets/cond_snowy.imageset/cond_snowy.svg
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M47.15 10.22C46.84 10.22 46.53 10.23 46.23 10.25C44.81 10.33 43.42 9.72 42.63 8.53C39.59 3.98 34.51 1 28.75 1C21.58 1 15.45 5.63 13.02 12.15C12.62 13.24 11.72 14.08 10.61 14.44C4.47 16.42 0 22.35 0 29.36C0 38 6.78 45 15.15 45H47.15C56.45 45 64 37.22 64 27.61C64 18.01 56.46 10.22 47.15 10.22Z" fill="white"/>
|
||||
<path d="M34.6995 58.07C35.5334 58.07 36.2095 57.394 36.2095 56.56C36.2095 55.7261 35.5334 55.05 34.6995 55.05C33.8655 55.05 33.1895 55.7261 33.1895 56.56C33.1895 57.394 33.8655 58.07 34.6995 58.07Z" fill="white"/>
|
||||
<path d="M38.4397 51.5901C39.2736 51.5901 39.9497 50.914 39.9497 50.0801C39.9497 49.2461 39.2736 48.5701 38.4397 48.5701C37.6057 48.5701 36.9297 49.2461 36.9297 50.0801C36.9297 50.914 37.6057 51.5901 38.4397 51.5901Z" fill="white"/>
|
||||
<path d="M42.8694 62.82C43.7033 62.82 44.3794 62.144 44.3794 61.31C44.3794 60.4761 43.7033 59.8 42.8694 59.8C42.0354 59.8 41.3594 60.4761 41.3594 61.31C41.3594 62.144 42.0354 62.82 42.8694 62.82Z" fill="white"/>
|
||||
<path d="M46.6096 56.3401C47.4436 56.3401 48.1196 55.664 48.1196 54.8301C48.1196 53.9961 47.4436 53.3201 46.6096 53.3201C45.7757 53.3201 45.0996 53.9961 45.0996 54.8301C45.0996 55.664 45.7757 56.3401 46.6096 56.3401Z" fill="white"/>
|
||||
<path d="M13.1799 58.07C14.0139 58.07 14.6899 57.394 14.6899 56.56C14.6899 55.7261 14.0139 55.05 13.1799 55.05C12.346 55.05 11.6699 55.7261 11.6699 56.56C11.6699 57.394 12.346 58.07 13.1799 58.07Z" fill="white"/>
|
||||
<path d="M16.9202 51.5901C17.7541 51.5901 18.4302 50.914 18.4302 50.0801C18.4302 49.2461 17.7541 48.5701 16.9202 48.5701C16.0862 48.5701 15.4102 49.2461 15.4102 50.0801C15.4102 50.914 16.0862 51.5901 16.9202 51.5901Z" fill="white"/>
|
||||
<path d="M21.3498 62.82C22.1838 62.82 22.8598 62.144 22.8598 61.31C22.8598 60.4761 22.1838 59.8 21.3498 59.8C20.5159 59.8 19.8398 60.4761 19.8398 61.31C19.8398 62.144 20.5159 62.82 21.3498 62.82Z" fill="white"/>
|
||||
<path d="M25.0901 56.3401C25.924 56.3401 26.6001 55.664 26.6001 54.8301C26.6001 53.9961 25.924 53.3201 25.0901 53.3201C24.2561 53.3201 23.5801 53.9961 23.5801 54.8301C23.5801 55.664 24.2561 56.3401 25.0901 56.3401Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
21
WhiteNights/Assets.xcassets/cond_sunny.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "cond_sunny.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
18
WhiteNights/Assets.xcassets/cond_sunny.imageset/cond_sunny.svg
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1_27514)">
|
||||
<path d="M19.3993 21.64C18.8293 21.64 18.2593 21.42 17.8193 20.99L9.3693 12.53C8.4993 11.66 8.4993 10.25 9.3693 9.38004C10.2393 8.51004 11.6493 8.51004 12.5193 9.38004L20.9693 17.83C21.8393 18.7 21.8393 20.11 20.9693 20.98C20.5393 21.42 19.9693 21.64 19.3993 21.64Z" fill="#FCD500"/>
|
||||
<path d="M61.7701 34.24H49.8101C48.5801 34.24 47.5801 33.24 47.5801 32.01C47.5801 30.78 48.5801 29.78 49.8101 29.78H61.7701C63.0001 29.78 64.0001 30.78 64.0001 32.01C64.0001 33.24 63.0001 34.24 61.7701 34.24Z" fill="#FCD500"/>
|
||||
<path d="M44.5997 21.64C44.0297 21.64 43.4597 21.42 43.0197 20.99C42.1497 20.12 42.1497 18.71 43.0197 17.84L51.4697 9.39005C52.3397 8.52005 53.7497 8.52005 54.6197 9.39005C55.4897 10.26 55.4897 11.67 54.6197 12.54L46.1697 20.99C45.7397 21.42 45.1697 21.64 44.5997 21.64Z" fill="#FCD500"/>
|
||||
<path d="M31.9995 16.42C30.7695 16.42 29.7695 15.42 29.7695 14.19V2.23C29.7695 1 30.7695 0 31.9995 0C33.2295 0 34.2295 1 34.2295 2.23V14.19C34.2295 15.42 33.2295 16.42 31.9995 16.42Z" fill="#FCD500"/>
|
||||
<path d="M14.19 34.24H2.24C1 34.24 0 33.24 0 32.01C0 30.78 1 29.78 2.23 29.78H14.18C15.41 29.78 16.41 30.78 16.41 32.01C16.41 33.24 15.42 34.24 14.19 34.24Z" fill="#FCD500"/>
|
||||
<path d="M10.9493 55.29C10.3793 55.29 9.8093 55.07 9.3693 54.6399C8.4993 53.7699 8.4993 52.36 9.3693 51.49L17.8193 43.04C18.6893 42.17 20.0993 42.17 20.9693 43.04C21.8393 43.91 21.8393 45.3199 20.9693 46.1899L12.5193 54.6399C12.0893 55.07 11.5193 55.29 10.9493 55.29Z" fill="#FCD500"/>
|
||||
<path d="M31.9995 64.0098C30.7695 64.0098 29.7695 63.0098 29.7695 61.7798V49.8198C29.7695 48.5898 30.7695 47.5898 31.9995 47.5898C33.2295 47.5898 34.2295 48.5898 34.2295 49.8198V61.7798C34.2295 63.0098 33.2295 64.0098 31.9995 64.0098Z" fill="#FCD500"/>
|
||||
<path d="M53.0497 55.29C52.4797 55.29 51.9097 55.07 51.4697 54.6399L43.0197 46.1899C42.1497 45.3199 42.1497 43.91 43.0197 43.04C43.8897 42.17 45.2997 42.17 46.1697 43.04L54.6197 51.49C55.4897 52.36 55.4897 53.7699 54.6197 54.6399C54.1897 55.07 53.6197 55.29 53.0497 55.29Z" fill="#FCD500"/>
|
||||
<path d="M32 43.9299C38.6252 43.9299 44 38.5551 44 31.9299C44 25.3047 38.6252 19.9299 32 19.9299C25.3748 19.9299 20 25.3047 20 31.9299C20 38.5551 25.3748 43.9299 32 43.9299Z" fill="#FCD500"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1_27514">
|
||||
<rect width="64" height="64" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
21
WhiteNights/Assets.xcassets/cond_thunder.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "cond_thunder.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
4
WhiteNights/Assets.xcassets/cond_thunder.imageset/cond_thunder.svg
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M47.15 12.22C46.84 12.22 46.53 12.23 46.23 12.25C44.81 12.33 43.42 11.72 42.63 10.53C39.59 5.98 34.51 3 28.75 3C21.58 3 15.45 7.63 13.02 14.15C12.62 15.24 11.72 16.08 10.61 16.44C4.47 18.42 0 24.35 0 31.35C0 39.99 6.78 46.99 15.15 46.99H47.15C56.45 46.99 64 39.21 64 29.6C64 20 56.46 12.22 47.15 12.22Z" fill="white"/>
|
||||
<path d="M26.0996 47.0101L47.6396 22.1001L44.5296 36.8901H56.9796L33.3696 61.8001L37.0596 47.0101H26.0996Z" fill="#FCD500"/>
|
||||
</svg>
|
After Width: | Height: | Size: 556 B |
21
WhiteNights/Assets.xcassets/det_humidity.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "det_humidity.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
3
WhiteNights/Assets.xcassets/det_humidity.imageset/det_humidity.svg
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M32 63.6799C19.42 63.6799 9.19 53.4499 9.19 40.8699C9.19 28.2499 22.63 9.86995 28.41 2.55995C29.28 1.44995 30.59 0.819946 32 0.819946C33.41 0.819946 34.72 1.44995 35.59 2.55995C41.37 9.87995 54.81 28.2599 54.81 40.8699C54.81 53.4399 44.58 63.6799 32 63.6799ZM32 4.80995C31.9 4.80995 31.7 4.83995 31.55 5.02995C27.24 10.4799 13.19 29.1799 13.19 40.8599C13.19 51.2299 21.63 59.6699 32 59.6699C42.37 59.6699 50.81 51.2299 50.81 40.8599C50.81 29.1799 36.76 10.4799 32.46 5.02995C32.3 4.83995 32.1 4.80995 32 4.80995Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 641 B |
21
WhiteNights/Assets.xcassets/det_wind_speed.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "det_wind.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
5
WhiteNights/Assets.xcassets/det_wind_speed.imageset/det_wind.svg
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.03 23.32H4.60001C3.50001 23.32 2.60001 22.42 2.60001 21.32C2.60001 20.22 3.50001 19.32 4.60001 19.32H22.02C26 19.32 29.24 16.08 29.24 12.1C29.24 8.12001 26 4.88 22.02 4.88C18.04 4.88 14.8 8.12001 14.8 12.1C14.8 13.2 13.9 14.1 12.8 14.1C11.7 14.1 10.8 13.2 10.8 12.1C10.8 5.91001 15.84 0.880005 22.02 0.880005C28.2 0.880005 33.24 5.92001 33.24 12.1C33.24 18.28 28.21 23.32 22.03 23.32Z" fill="white"/>
|
||||
<path d="M50.17 34.3H14.15C13.05 34.3 12.15 33.4 12.15 32.3C12.15 31.2 13.05 30.3 14.15 30.3H50.17C54.15 30.3 57.39 27.06 57.39 23.08C57.39 19.1 54.15 15.86 50.17 15.86C46.19 15.86 42.95 19.1 42.95 23.08C42.95 24.18 42.05 25.08 40.95 25.08C39.85 25.08 38.95 24.18 38.95 23.08C38.95 16.89 43.99 11.86 50.17 11.86C56.36 11.86 61.39 16.9 61.39 23.08C61.39 29.26 56.36 34.3 50.17 34.3Z" fill="white"/>
|
||||
<path d="M40.63 63.1299C34.44 63.1299 29.41 58.0899 29.41 51.9099C29.41 50.8099 30.31 49.9099 31.41 49.9099C32.51 49.9099 33.41 50.8099 33.41 51.9099C33.41 55.8899 36.65 59.1299 40.63 59.1299C44.61 59.1299 47.85 55.8899 47.85 51.9099C47.85 47.9299 44.61 44.6899 40.63 44.6899H4.60001C3.50001 44.6899 2.60001 43.7899 2.60001 42.6899C2.60001 41.5899 3.50001 40.6899 4.60001 40.6899H40.62C46.81 40.6899 51.84 45.7299 51.84 51.9099C51.84 58.0899 46.82 63.1299 40.63 63.1299Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
21
WhiteNights/Assets.xcassets/en_lang_icon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "en_lang_icon.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
3
WhiteNights/Assets.xcassets/en_lang_icon.imageset/en_lang_icon.svg
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 0C7.16667 0 0 7.16667 0 16C0 24.8333 7.16667 32 16 32C24.8333 32 32 24.8333 32 16C32 7.16667 24.8333 0 16 0ZM14.38 22.5267H5.60667V9.43333H14.3667V11.62H8.3V14.74H13.48V16.8533H8.3V20.36H14.38V22.5267ZM26.36 22.5267H23.66L18.4067 13.9133V22.5267H15.7067V9.43333H18.4067L23.6667 18.0667V9.43333H26.3533V22.5267H26.36Z" fill="#CCCCCC"/>
|
||||
</svg>
|
After Width: | Height: | Size: 451 B |
21
WhiteNights/Assets.xcassets/ru_lang_icon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "ru_lang_icon.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
4
WhiteNights/Assets.xcassets/ru_lang_icon.imageset/ru_lang_icon.svg
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 0C7.16667 0 0 7.16667 0 16C0 24.8333 7.16667 32 16 32C24.8333 32 32 24.8333 32 16C32 7.16667 24.8333 0 16 0ZM16.1333 22.3667H13.28L10.86 17.64H8.74V22.3667H6.08V9.45333H10.88C12.4067 9.45333 13.58 9.79333 14.4133 10.4733C15.2467 11.1533 15.6533 12.1133 15.6533 13.3533C15.6533 14.2333 15.46 14.9667 15.08 15.56C14.7 16.1467 14.12 16.62 13.3467 16.9667L16.14 22.2467V22.3733L16.1333 22.3667ZM26.8667 17.96C26.8667 19.3733 26.4267 20.4933 25.54 21.3133C24.6533 22.1333 23.4467 22.5467 21.9133 22.5467C20.38 22.5467 19.2067 22.1467 18.32 21.3467C17.4333 20.5467 16.98 19.4533 16.96 18.0533V9.45333H19.62V17.98C19.62 18.8267 19.82 19.44 20.2267 19.8267C20.6333 20.2133 21.1933 20.4067 21.9067 20.4067C23.4 20.4067 24.16 19.62 24.1867 18.0467V9.45333H26.8533V17.96H26.8667Z" fill="#CCCCCC"/>
|
||||
<path d="M10.8724 11.6066H8.73242V15.4799H10.8791C11.5458 15.4799 12.0658 15.3066 12.4324 14.9666C12.7991 14.6266 12.9791 14.1599 12.9791 13.5599C12.9791 12.9599 12.8058 12.4733 12.4591 12.1199C12.1124 11.7666 11.5791 11.5933 10.8658 11.5933L10.8724 11.6066Z" fill="#CCCCCC"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
21
WhiteNights/Assets.xcassets/zh_lang_icon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "zh_lang_icon.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
6
WhiteNights/Assets.xcassets/zh_lang_icon.imageset/zh_lang_icon.svg
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.858 13.588H4.194V16.098H6.858V13.588Z" fill="#CCCCCC"/>
|
||||
<path d="M9.13599 16.098H11.814V13.588H9.13599V16.098Z" fill="#CCCCCC"/>
|
||||
<path d="M24.0836 13.364H19.905C20.4404 14.6271 21.1458 15.7628 22.0342 16.7478C22.8737 15.7952 23.5544 14.675 24.0836 13.364Z" fill="#CCCCCC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 32C24.8365 32 32 24.8365 32 16C32 7.16345 24.8365 0 16 0C7.16345 0 0 7.16345 0 16C0 24.8365 7.16345 32 16 32ZM6.858 9H9.13599V11.436H14.078V18.964H11.814V18.25H9.13599V22.646H6.858V18.25H4.194V19.0341H2V11.436H6.858V9ZM20.9 9H23.136V11.212H28.722V13.364H26.5924C25.8694 15.3004 24.9164 16.9222 23.7207 18.271C25.0992 19.2825 26.7767 20.0361 28.7788 20.4861L29.2313 20.5878L28.8976 20.9099C28.5169 21.2775 27.9851 22.0514 27.7257 22.5293L27.6323 22.7014L27.4428 22.6519C25.2257 22.0725 23.4166 21.1681 21.9316 19.9502C20.4197 21.1302 18.6259 22.0223 16.5346 22.6766L16.3259 22.7419L16.2335 22.5437C16.0482 22.1469 15.5289 21.3527 15.2034 20.9594L14.9538 20.6578L15.3324 20.5582C17.2907 20.0432 18.935 19.3135 20.2915 18.329C19.1183 16.9374 18.2127 15.2761 17.4941 13.364H15.398V11.212H20.9V9Z" fill="#CCCCCC"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -1,24 +1,84 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// WhiteNights
|
||||
//
|
||||
// Created by Микаэл Оганесян on 24.08.2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@State private var showMenu = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Image(systemName: "globe")
|
||||
.imageScale(.large)
|
||||
.foregroundStyle(.tint)
|
||||
Text("Hello, world!")
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
VStack(spacing: 10) {
|
||||
HStack(spacing: 10) {
|
||||
RouteView()
|
||||
WeatherView()
|
||||
}
|
||||
|
||||
if let sightId = appState.sightId {
|
||||
SightView(sightId: sightId)
|
||||
} else {
|
||||
Text("Выберите маршрут, чтобы увидеть достопримечательность")
|
||||
.foregroundColor(.gray)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
// Плавающая кнопка в правом нижнем углу
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: {
|
||||
withAnimation(.spring()) {
|
||||
showMenu.toggle()
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "line.3.horizontal")
|
||||
.font(.system(size: 24, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.clipShape(Circle())
|
||||
.shadow(radius: 5)
|
||||
}
|
||||
.padding(.trailing, 20)
|
||||
// .padding(.bottom, -20) // Убираем отрицательный паддинг
|
||||
.padding(.bottom, -20) // Добавляем паддинг, чтобы кнопка не перекрывалась
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Используем кастомное BottomMenu
|
||||
if showMenu {
|
||||
// Используем Binding для двусторонней связи
|
||||
BottomMenu(isPresented: $showMenu)
|
||||
.transition(.move(edge: .bottom))
|
||||
// Добавляем игнорирование safe area сверху, если BottomMenu
|
||||
// должно полностью закрывать контент, но обычно для
|
||||
// BottomSheet это не требуется, GeometryReader в BottomMenu
|
||||
// уже делает нужное растяжение.
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await fetchRoutes()
|
||||
}
|
||||
.preferredColorScheme(.light)
|
||||
}
|
||||
|
||||
private func fetchRoutes() async {
|
||||
guard let url = URL(string: "https://white-nights.krbl.ru/services/content/route") else { return }
|
||||
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
let routes = try JSONDecoder().decode([Route].self, from: data)
|
||||
|
||||
appState.allRoutes = routes
|
||||
if let firstRoute = routes.first {
|
||||
appState.selectedRoute = firstRoute
|
||||
}
|
||||
} catch {
|
||||
print("Ошибка загрузки маршрутов: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
17
WhiteNights/Models/Article.swift
Normal file
@ -0,0 +1,17 @@
|
||||
import Foundation
|
||||
|
||||
struct Article: Decodable, Identifiable, Equatable {
|
||||
let id: Int
|
||||
let body: String
|
||||
let heading: String
|
||||
let service_name: String?
|
||||
var isReviewArticle: Bool? = false
|
||||
|
||||
init(id: Int, body: String, heading: String, service_name: String? = nil, isReviewArticle: Bool = false) {
|
||||
self.id = id
|
||||
self.body = body
|
||||
self.heading = heading
|
||||
self.service_name = service_name
|
||||
self.isReviewArticle = isReviewArticle
|
||||
}
|
||||
}
|
8
WhiteNights/Models/ArticleMedia.swift
Normal file
@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
struct ArticleMedia: Decodable, Identifiable {
|
||||
let id: String
|
||||
let filename: String
|
||||
let media_name: String
|
||||
let media_type: Int
|
||||
}
|
11
WhiteNights/Models/Route.swift
Normal file
@ -0,0 +1,11 @@
|
||||
struct Route: Decodable, Identifiable {
|
||||
let id: Int
|
||||
let routeNumber: String
|
||||
let routeSysNumber: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case routeNumber = "route_number"
|
||||
case routeSysNumber = "route_sys_number"
|
||||
}
|
||||
}
|
33
WhiteNights/Models/SightModel.swift
Normal file
@ -0,0 +1,33 @@
|
||||
import Foundation
|
||||
|
||||
struct SightModel: Decodable, Identifiable {
|
||||
let id: Int
|
||||
let address: String
|
||||
let city: String?
|
||||
let city_id: Int
|
||||
let latitude: Double
|
||||
let longitude: Double
|
||||
let left_article: Int
|
||||
let name: String
|
||||
let preview_media: String?
|
||||
let thumbnail: String
|
||||
let video_preview: String?
|
||||
let watermark_lu: String
|
||||
let watermark_rd: String
|
||||
|
||||
var previewMediaURL: URL? {
|
||||
guard let preview_media = preview_media else { return nil }
|
||||
return URL(string: preview_media)
|
||||
}
|
||||
|
||||
var videoPreviewURL: URL? {
|
||||
guard let video_preview = video_preview else { return nil }
|
||||
return URL(string: video_preview)
|
||||
}
|
||||
}
|
||||
|
||||
struct SightContent: Decodable {
|
||||
let name: String
|
||||
let preview_media: String
|
||||
let video_preview: String?
|
||||
}
|
6
WhiteNights/Models/Station.swift
Normal file
@ -0,0 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
struct Station: Codable, Identifiable {
|
||||
var id: Int
|
||||
var name: String
|
||||
}
|
27
WhiteNights/Models/StopModels.swift
Normal file
@ -0,0 +1,27 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - Stop
|
||||
struct Stop: Identifiable, Codable {
|
||||
let id: Int
|
||||
let name: String
|
||||
}
|
||||
|
||||
// MARK: - StopDetail
|
||||
struct StopDetail: Identifiable, Codable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let transfers: Transfers
|
||||
}
|
||||
|
||||
// MARK: - Transfers
|
||||
struct Transfers: Codable {
|
||||
let tram: String?
|
||||
let trolleybus: String?
|
||||
let bus: String?
|
||||
let train: String?
|
||||
let metroRed: String?
|
||||
let metroGreen: String?
|
||||
let metroBlue: String?
|
||||
let metroPurple: String?
|
||||
let metroOrange: String?
|
||||
}
|
22
WhiteNights/Models/WeaherModel.swift
Normal file
@ -0,0 +1,22 @@
|
||||
import Foundation
|
||||
|
||||
struct WeatherResponse: Codable {
|
||||
let currentWeather: CurrentWeatherItem?
|
||||
let forecast: [ForecastItem]
|
||||
}
|
||||
|
||||
struct CurrentWeatherItem: Codable {
|
||||
let temperatureCelsius: Double
|
||||
let description: String
|
||||
let humidity: Int
|
||||
let windSpeed: Double
|
||||
}
|
||||
|
||||
struct ForecastItem: Codable {
|
||||
let date: String
|
||||
let description: String
|
||||
let humidity: Int
|
||||
let windSpeed: Double
|
||||
let minTemperatureCelsius: Double
|
||||
let maxTemperatureCelsius: Double
|
||||
}
|
13
WhiteNights/Utils/Color+Extensions.swift
Normal file
@ -0,0 +1,13 @@
|
||||
import SwiftUI
|
||||
|
||||
extension Color {
|
||||
init(hex: UInt, alpha: Double = 1) {
|
||||
self.init(
|
||||
.sRGB,
|
||||
red: Double((hex >> 16) & 0xFF)/255,
|
||||
green: Double((hex >> 8) & 0xFF)/255,
|
||||
blue: Double(hex & 0xFF)/255,
|
||||
opacity: alpha
|
||||
)
|
||||
}
|
||||
}
|
71
WhiteNights/Utils/MarqueeText.swift
Normal file
@ -0,0 +1,71 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MarqueeText: View {
|
||||
let text: String
|
||||
let font: Font
|
||||
let foregroundColor: Color
|
||||
private let speed: CGFloat = 10 // пикселей в секунду
|
||||
|
||||
@State private var textWidth: CGFloat = 0
|
||||
@State private var containerWidth: CGFloat = 0
|
||||
@State private var offset: CGFloat = 0
|
||||
@State private var animationStarted = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
Text(text)
|
||||
.font(font)
|
||||
.foregroundColor(foregroundColor)
|
||||
.lineLimit(1)
|
||||
.fixedSize() // важно, чтобы не обрезался
|
||||
.background(WidthGetter())
|
||||
.offset(x: offset)
|
||||
.onAppear {
|
||||
containerWidth = geo.size.width
|
||||
if textWidth > containerWidth, !animationStarted {
|
||||
animationStarted = true
|
||||
let distance = textWidth - containerWidth
|
||||
let duration = Double(distance / speed)
|
||||
withAnimation(.linear(duration: duration).repeatForever(autoreverses: true)) {
|
||||
offset = -distance
|
||||
}
|
||||
}
|
||||
}
|
||||
.clipped()
|
||||
}
|
||||
.frame(height: lineHeight(for: font))
|
||||
.onPreferenceChange(WidthKey.self) { width in
|
||||
textWidth = width
|
||||
}
|
||||
}
|
||||
|
||||
private func lineHeight(for font: Font) -> CGFloat {
|
||||
switch font {
|
||||
case .largeTitle: return 34
|
||||
case .title: return 28
|
||||
case .title2: return 22
|
||||
case .title3: return 20
|
||||
case .headline: return 17
|
||||
case .body: return 17
|
||||
case .callout: return 16
|
||||
case .subheadline: return 15
|
||||
case .caption: return 13
|
||||
case .caption2: return 12
|
||||
default: return 17
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: — Helpers для измерения ширины текста
|
||||
private struct WidthKey: PreferenceKey {
|
||||
static var defaultValue: CGFloat = 0
|
||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() }
|
||||
}
|
||||
|
||||
private struct WidthGetter: View {
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
Color.clear.preference(key: WidthKey.self, value: geo.size.width)
|
||||
}
|
||||
}
|
||||
}
|
17
WhiteNights/Utils/View+CornerRadius.swift
Normal file
@ -0,0 +1,17 @@
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
|
||||
clipShape(RoundedCorner(radius: radius, corners: corners))
|
||||
}
|
||||
}
|
||||
|
||||
private struct RoundedCorner: Shape {
|
||||
var radius: CGFloat = .infinity
|
||||
var corners: UIRectCorner = .allCorners
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
|
||||
return Path(path.cgPath)
|
||||
}
|
||||
}
|
39
WhiteNights/Utils/View+Extensions.swift
Normal file
@ -0,0 +1,39 @@
|
||||
import SwiftUI
|
||||
|
||||
struct BlockStyle: ViewModifier {
|
||||
// Добавляем свойство для хранения радиуса скругления
|
||||
let cornerRadius: CGFloat
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.background(
|
||||
ZStack {
|
||||
Color(hex: 0x806C59)
|
||||
LinearGradient(
|
||||
stops: [
|
||||
.init(color: .white.opacity(0.0), location: 0.0871),
|
||||
.init(color: .white.opacity(0.16), location: 0.6969)
|
||||
],
|
||||
startPoint: .bottomLeading,
|
||||
endPoint: .topTrailing
|
||||
)
|
||||
}
|
||||
.cornerRadius(cornerRadius) // Применяем скругление к фону
|
||||
)
|
||||
.shadow(
|
||||
color: Color.black.opacity(0.10),
|
||||
radius: 8,
|
||||
x: 0,
|
||||
y: 2
|
||||
)
|
||||
// Применяем скругление к содержимому (опционально, но лучше для теней)
|
||||
.cornerRadius(cornerRadius)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
// Изменяем расширение, чтобы оно принимало параметр cornerRadius
|
||||
func blockStyle(cornerRadius: CGFloat) -> some View {
|
||||
modifier(BlockStyle(cornerRadius: cornerRadius))
|
||||
}
|
||||
}
|
@ -9,9 +9,12 @@ import SwiftUI
|
||||
|
||||
@main
|
||||
struct WhiteNightsApp: App {
|
||||
@StateObject private var appState = AppState()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.environmentObject(appState) // <- обязательно!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
297
WhiteNights/Widgets/BottomMenu.swift
Normal file
@ -0,0 +1,297 @@
|
||||
import SwiftUI
|
||||
import SDWebImageSwiftUI
|
||||
|
||||
private extension String {
|
||||
var trimmedNonEmpty: String? {
|
||||
let t = trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return t.isEmpty ? nil : t
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ViewModel
|
||||
@MainActor
|
||||
final class StopsViewModel: ObservableObject {
|
||||
@Published var stops: [StopDetail] = []
|
||||
@Published var isLoading: Bool = false
|
||||
@Published var selectedStopId: Int?
|
||||
|
||||
func fetchStops(for routeId: Int) {
|
||||
guard let url = URL(string: "https://white-nights.krbl.ru/services/content/route/\(routeId)/station") else { return }
|
||||
isLoading = true
|
||||
|
||||
Task {
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
let stops = try decoder.decode([StopDetail].self, from: data)
|
||||
self.stops = stops
|
||||
} catch {
|
||||
print("Ошибка загрузки остановок:", error)
|
||||
self.stops = []
|
||||
}
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
func toggleStop(id: Int) {
|
||||
withAnimation(.easeInOut(duration: 0.25)) {
|
||||
if selectedStopId == id {
|
||||
selectedStopId = nil
|
||||
} else {
|
||||
selectedStopId = id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BottomMenu
|
||||
struct BottomMenu: View {
|
||||
@Binding var isPresented: Bool
|
||||
@State private var dragOffset: CGFloat = 0
|
||||
@State private var selectedTab: Tab = .sights
|
||||
@EnvironmentObject var appState: AppState
|
||||
@StateObject private var stopsVM = StopsViewModel()
|
||||
|
||||
enum Tab { case sights, stops }
|
||||
|
||||
private let columns: [GridItem] = Array(
|
||||
repeating: GridItem(.flexible(), spacing: 16, alignment: .top),
|
||||
count: 3
|
||||
)
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
ZStack {
|
||||
if isPresented {
|
||||
Color.black.opacity(0.4)
|
||||
.ignoresSafeArea()
|
||||
.onTapGesture { isPresented = false }
|
||||
.transition(.opacity)
|
||||
}
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 20) {
|
||||
Capsule()
|
||||
.fill(Color.white.opacity(0.8))
|
||||
.frame(width: 120, height: 3)
|
||||
.padding(.top, 8)
|
||||
|
||||
VStack(spacing: 12) {
|
||||
menuButton(title: "Достопримечательности", tab: .sights)
|
||||
menuButton(title: "Остановки", tab: .stops)
|
||||
|
||||
if selectedTab == .sights {
|
||||
// --- Достопримечательности ---
|
||||
ScrollView {
|
||||
LazyVGrid(columns: columns, spacing: 20) {
|
||||
ForEach(appState.sights) { sight in
|
||||
VStack(spacing: 8) {
|
||||
SightThumbnail(
|
||||
url: URL(string: "https://white-nights.krbl.ru/services/content/media/\(sight.thumbnail)/download"),
|
||||
size: 80
|
||||
)
|
||||
.onTapGesture {
|
||||
appState.sightId = sight.id
|
||||
isPresented = false
|
||||
}
|
||||
|
||||
Text(sight.name)
|
||||
.font(.footnote)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(2)
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
} else {
|
||||
// --- Остановки + пересадки ---
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 0) {
|
||||
if stopsVM.isLoading {
|
||||
ProgressView("Загрузка остановок...")
|
||||
.padding()
|
||||
} else {
|
||||
ForEach(stopsVM.stops) { stop in
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Button {
|
||||
stopsVM.toggleStop(id: stop.id)
|
||||
} label: {
|
||||
Text(stop.name)
|
||||
.font(.subheadline) // меньше размер
|
||||
.frame(maxWidth: .infinity, alignment: .leading) // левое выравнивание
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 20)
|
||||
.foregroundColor(.white)
|
||||
.transaction { $0.animation = nil }
|
||||
}
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundColor(.white.opacity(0.3)),
|
||||
alignment: .bottom
|
||||
)
|
||||
|
||||
if stopsVM.selectedStopId == stop.id {
|
||||
transfersView(for: stop)
|
||||
.transition(.opacity.combined(with: .move(edge: .top)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if stopsVM.stops.isEmpty,
|
||||
let routeId = appState.selectedRoute?.id {
|
||||
stopsVM.fetchStops(for: routeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 2)
|
||||
|
||||
HStack(spacing: 16) {
|
||||
Image("GAT_Icon")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(height: 30)
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 2) {
|
||||
Image("ru_lang_icon").resizable().scaledToFit().frame(width: 24, height: 24)
|
||||
Image("zh_lang_icon").resizable().scaledToFit().frame(width: 24, height: 24)
|
||||
Image("en_lang_icon").resizable().scaledToFit().frame(width: 24, height: 24)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 26)
|
||||
.padding(.top, 16)
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
.frame(height: geo.size.height * 0.8)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(
|
||||
Color(hex: 0x806C59)
|
||||
.cornerRadius(24, corners: [.topLeft, .topRight])
|
||||
.shadow(radius: 10)
|
||||
)
|
||||
.offset(y: isPresented ? dragOffset : geo.size.height)
|
||||
.animation(.spring(response: 0.35, dampingFraction: 0.9), value: isPresented)
|
||||
.animation(.spring(response: 0.35, dampingFraction: 0.9), value: dragOffset)
|
||||
.gesture(
|
||||
DragGesture()
|
||||
.onChanged { value in
|
||||
if value.translation.height > 0 {
|
||||
dragOffset = value.translation.height
|
||||
}
|
||||
}
|
||||
.onEnded { value in
|
||||
if value.translation.height > 100 { isPresented = false }
|
||||
dragOffset = 0
|
||||
}
|
||||
)
|
||||
}
|
||||
.ignoresSafeArea(edges: .bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Menu Button
|
||||
@ViewBuilder
|
||||
private func menuButton(title: String, tab: Tab) -> some View {
|
||||
Button { selectedTab = tab } label: {
|
||||
Text(title)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 47)
|
||||
.foregroundColor(.white)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(selectedTab == tab ? Color(hex: 0x6D5743) : Color(hex: 0xB3A598))
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
// MARK: - Transfers View
|
||||
@ViewBuilder
|
||||
private func transfersView(for stop: StopDetail) -> some View {
|
||||
let t = stop.transfers
|
||||
let items: [(String, String)] = [
|
||||
("tram_icon", t.tram?.trimmedNonEmpty),
|
||||
("trolley_icon", t.trolleybus?.trimmedNonEmpty),
|
||||
("bus_icon", t.bus?.trimmedNonEmpty),
|
||||
("train_icon", t.train?.trimmedNonEmpty),
|
||||
("metro_red_icon", t.metroRed?.trimmedNonEmpty),
|
||||
("metro_green_icon", t.metroGreen?.trimmedNonEmpty),
|
||||
("metro_blue_icon", t.metroBlue?.trimmedNonEmpty),
|
||||
("metro_purple_icon", t.metroPurple?.trimmedNonEmpty),
|
||||
("metro_orange_icon", t.metroOrange?.trimmedNonEmpty),
|
||||
].compactMap { icon, text in
|
||||
guard let text = text else { return nil }
|
||||
return (icon, text)
|
||||
}
|
||||
|
||||
if items.isEmpty {
|
||||
Text("Нет пересадок")
|
||||
.font(.caption) // меньше размер
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
.padding(.leading, 20)
|
||||
.padding(.vertical, 8)
|
||||
} else {
|
||||
VStack(alignment: .leading, spacing: 6) { // меньше расстояние
|
||||
ForEach(items, id: \.0) { icon, text in
|
||||
HStack(spacing: 8) {
|
||||
Image(icon)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 18, height: 18)
|
||||
Text(text)
|
||||
.font(.caption) // меньше размер
|
||||
.foregroundColor(.white)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(maxWidth: .infinity, alignment: .leading) // левое выравнивание
|
||||
}
|
||||
.padding(.leading, 20)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 8)
|
||||
.padding(.top, 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sight Thumbnail
|
||||
struct SightThumbnail: View {
|
||||
let url: URL?
|
||||
let size: CGFloat
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.white)
|
||||
.frame(width: size, height: size)
|
||||
|
||||
WebImage(url: url)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: size, height: size)
|
||||
.clipShape(Circle())
|
||||
.overlay(
|
||||
Group {
|
||||
if url == nil {
|
||||
ProgressView().frame(width: size, height: size)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
52
WhiteNights/Widgets/RouteSelectionView.swift
Normal file
@ -0,0 +1,52 @@
|
||||
import SwiftUI
|
||||
|
||||
struct RouteSelectionView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
@State private var routes: [Route] = []
|
||||
@State private var isLoading = true
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if isLoading {
|
||||
ProgressView("Загрузка маршрутов...")
|
||||
.padding()
|
||||
} else {
|
||||
List(routes, id: \.id) { route in
|
||||
Button(action: {
|
||||
appState.selectedRoute = route
|
||||
}) {
|
||||
HStack {
|
||||
Text("\(route.routeNumber)")
|
||||
.font(.headline)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
.listStyle(PlainListStyle())
|
||||
}
|
||||
}
|
||||
.navigationTitle("Выберите маршрут")
|
||||
.onAppear {
|
||||
Task {
|
||||
await fetchRoutes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fetch Routes
|
||||
private func fetchRoutes() async {
|
||||
isLoading = true
|
||||
defer { isLoading = false }
|
||||
|
||||
guard let url = URL(string: "https://white-nights.krbl.ru/services/content/route") else { return }
|
||||
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
let fetchedRoutes = try JSONDecoder().decode([Route].self, from: data)
|
||||
routes = fetchedRoutes
|
||||
} catch {
|
||||
print("Ошибка загрузки маршрутов: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
108
WhiteNights/Widgets/RouteView.swift
Normal file
@ -0,0 +1,108 @@
|
||||
import SwiftUI
|
||||
|
||||
struct RouteView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
@State private var firstStationName: String = "Загрузка..."
|
||||
@State private var lastStationName: String = "Загрузка..."
|
||||
@State private var engStationsName: String = "Загрузка..."
|
||||
|
||||
private var topBackgroundColor = Color(hex: 0xFCD500)
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(destination: RouteSelectionView()) {
|
||||
GeometryReader { geo in
|
||||
VStack(spacing: 0) {
|
||||
|
||||
// Верхняя половина: номер маршрута
|
||||
VStack {
|
||||
if let route = appState.selectedRoute {
|
||||
Text("\(route.routeNumber)")
|
||||
.font(.system(size: 36, weight: .black))
|
||||
.foregroundColor(.black)
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxHeight: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
.frame(height: geo.size.height / 2)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(topBackgroundColor)
|
||||
.cornerRadius(16, corners: [.topLeft, .topRight])
|
||||
|
||||
// Нижняя половина: станции
|
||||
VStack(spacing: 0) {
|
||||
MarqueeText(
|
||||
text: firstStationName,
|
||||
font: .headline.bold(),
|
||||
foregroundColor: .white
|
||||
)
|
||||
MarqueeText(
|
||||
text: lastStationName,
|
||||
font: .headline.bold(),
|
||||
foregroundColor: .white
|
||||
)
|
||||
MarqueeText(
|
||||
text: engStationsName,
|
||||
font: .caption,
|
||||
foregroundColor: .white.opacity(0.5)
|
||||
)
|
||||
}
|
||||
.padding(10)
|
||||
.frame(height: geo.size.height / 2)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(
|
||||
ZStack {
|
||||
Color(hex: 0x806C59)
|
||||
LinearGradient(
|
||||
stops: [
|
||||
.init(color: .white.opacity(0.0), location: 0.0871),
|
||||
.init(color: .white.opacity(0.16), location: 0.6969)
|
||||
],
|
||||
startPoint: .bottomLeading,
|
||||
endPoint: .topTrailing
|
||||
)
|
||||
}
|
||||
)
|
||||
.cornerRadius(16, corners: [.bottomLeft, .bottomRight])
|
||||
}
|
||||
}
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.shadow(color: Color.black.opacity(0.10), radius: 8, x: 0, y: 2)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.onChange(of: appState.selectedRoute?.id) { newRouteID in
|
||||
guard let routeID = newRouteID else { return }
|
||||
Task {
|
||||
await fetchStations(forRoute: routeID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fetch Stations
|
||||
private func fetchStations(forRoute routeID: Int) async {
|
||||
firstStationName = "Загрузка..."
|
||||
lastStationName = "Загрузка..."
|
||||
engStationsName = "Loading..."
|
||||
|
||||
guard let url = URL(string: "https://white-nights.krbl.ru/services/content/route/\(routeID)/station"),
|
||||
let urlEng = URL(string: "https://white-nights.krbl.ru/services/content/route/\(routeID)/station?lang=en") else { return }
|
||||
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
let (dataEn, _) = try await URLSession.shared.data(from: urlEng)
|
||||
|
||||
let stations = try JSONDecoder().decode([Station].self, from: data)
|
||||
let stationsEn = try JSONDecoder().decode([Station].self, from: dataEn)
|
||||
|
||||
if let firstStation = stations.first { firstStationName = firstStation.name }
|
||||
if let lastStation = stations.last { lastStationName = lastStation.name }
|
||||
|
||||
let firstStationEn = stationsEn.first?.name ?? "Loading..."
|
||||
let lastStationEn = stationsEn.last?.name ?? "Loading..."
|
||||
engStationsName = "\(firstStationEn) - \(lastStationEn)"
|
||||
|
||||
} catch {
|
||||
print("Ошибка загрузки станций: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
137
WhiteNights/Widgets/SightView.swift
Normal file
@ -0,0 +1,137 @@
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
import NukeUI
|
||||
import UIKit
|
||||
|
||||
|
||||
// MARK: - SightView
|
||||
|
||||
struct SightView: View {
|
||||
let sightId: Int
|
||||
@StateObject private var viewModel = SightViewModel()
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
mediaSection
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
// Заголовок статьи
|
||||
Text(viewModel.selectedArticle?.isReviewArticle == true ? viewModel.sightName : viewModel.articleHeading)
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity, alignment: viewModel.selectedArticle?.isReviewArticle == true ? .center : .leading)
|
||||
|
||||
// Тело статьи
|
||||
ScrollView {
|
||||
if viewModel.selectedArticle?.isReviewArticle == true {
|
||||
VStack {
|
||||
Text(viewModel.articleBody)
|
||||
.font(.system(size: 13))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
} else {
|
||||
Text(viewModel.articleBody)
|
||||
.font(.system(size: 13))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
// Список статей (кнопки навигации) - ФИНАЛЬНОЕ ИСПРАВЛЕНИЕ ЦЕНТРИРОВАНИЯ
|
||||
GeometryReader { geometry in
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 6) {
|
||||
// Spacers для центрирования
|
||||
Spacer(minLength: 0)
|
||||
|
||||
ForEach(viewModel.allArticles) { article in
|
||||
Text(article.heading)
|
||||
.font(.system(size: 12))
|
||||
.lineLimit(1)
|
||||
.padding(.vertical, 6)
|
||||
.padding(.horizontal, 6)
|
||||
.frame(minWidth: 70)
|
||||
.foregroundColor(.white)
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.frame(height: 2)
|
||||
.foregroundColor(viewModel.selectedArticle == article ? Color.white : Color.clear),
|
||||
alignment: .bottom
|
||||
)
|
||||
.onTapGesture {
|
||||
viewModel.selectArticle(article)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
// Принудительно задаем ширину HStack как ширину GeometryReader
|
||||
.frame(minWidth: geometry.size.width)
|
||||
}
|
||||
.scrollIndicators(.hidden) // Скрываем полосу прокрутки
|
||||
}
|
||||
// Задаем высоту для GeometryReader
|
||||
.frame(height: 34)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.bottom, 10)
|
||||
}
|
||||
.task(id: sightId) {
|
||||
await viewModel.loadInitialData(sightId: sightId)
|
||||
}
|
||||
.blockStyle(cornerRadius: 25)
|
||||
}
|
||||
|
||||
// Медиа-секция
|
||||
@ViewBuilder
|
||||
private var mediaSection: some View {
|
||||
Group {
|
||||
switch viewModel.mediaState {
|
||||
case .loading:
|
||||
ZStack {
|
||||
Color.gray.opacity(0.3)
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.tint(.white)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.aspectRatio(16/9, contentMode: .fit)
|
||||
.cornerRadius(24, corners: [.topLeft, .topRight])
|
||||
.clipped()
|
||||
|
||||
case .image(let url):
|
||||
LazyImage(url: url) { state in
|
||||
if let image = state.image {
|
||||
image
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
} else {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.cornerRadius(24, corners: [.topLeft, .topRight])
|
||||
.clipped()
|
||||
|
||||
case .video(let player):
|
||||
VideoPlayer(player: player)
|
||||
.aspectRatio(16/9, contentMode: .fit)
|
||||
.cornerRadius(24, corners: [.topLeft, .topRight])
|
||||
.clipped()
|
||||
|
||||
case .error:
|
||||
Image(systemName: "photo")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(.gray)
|
||||
.frame(maxWidth: .infinity, minHeight: 200)
|
||||
.cornerRadius(24, corners: [.topLeft, .topRight])
|
||||
.clipped()
|
||||
}
|
||||
}
|
||||
.padding(4)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
98
WhiteNights/Widgets/SightViewModel.swift
Normal file
@ -0,0 +1,98 @@
|
||||
import Foundation
|
||||
import AVKit
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
class SightViewModel: ObservableObject {
|
||||
@Published var sightName: String = "Загрузка..."
|
||||
@Published var allArticles: [Article] = []
|
||||
@Published var selectedArticle: Article?
|
||||
@Published var articleHeading: String = ""
|
||||
@Published var articleBody: String = ""
|
||||
@Published var mediaState: MediaState = .loading
|
||||
|
||||
private var sightModel: SightModel?
|
||||
|
||||
enum MediaState {
|
||||
case loading
|
||||
case image(URL)
|
||||
case video(AVPlayer)
|
||||
case error
|
||||
}
|
||||
|
||||
func loadInitialData(sightId: Int) async {
|
||||
do {
|
||||
async let sightModelTask = fetchJSON(from: "https://white-nights.krbl.ru/services/content/sight/\(sightId)", type: SightModel.self)
|
||||
async let articlesTask = fetchJSON(from: "https://white-nights.krbl.ru/services/content/sight/\(sightId)/article", type: [Article].self)
|
||||
|
||||
let (fetchedSightModel, fetchedArticles) = try await (sightModelTask, articlesTask)
|
||||
|
||||
self.sightModel = fetchedSightModel
|
||||
self.sightName = fetchedSightModel.name
|
||||
|
||||
let reviewArticle = Article(id: -1, body: "", heading: "Обзор", isReviewArticle: true)
|
||||
self.allArticles = [reviewArticle] + fetchedArticles
|
||||
|
||||
selectArticle(reviewArticle)
|
||||
|
||||
} catch {
|
||||
print("Ошибка начальной загрузки данных: \(error)")
|
||||
self.mediaState = .error
|
||||
}
|
||||
}
|
||||
|
||||
func selectArticle(_ article: Article) {
|
||||
guard selectedArticle != article else { return }
|
||||
|
||||
self.selectedArticle = article
|
||||
self.articleHeading = article.heading
|
||||
self.articleBody = article.body
|
||||
|
||||
Task {
|
||||
await updateMedia(for: article)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateMedia(for article: Article) async {
|
||||
self.mediaState = .loading
|
||||
|
||||
if article.isReviewArticle == true {
|
||||
guard let sight = sightModel else {
|
||||
self.mediaState = .error
|
||||
return
|
||||
}
|
||||
|
||||
if let videoPreviewId = sight.video_preview,
|
||||
let url = URL(string: "https://white-nights.krbl.ru/services/content/media/\(videoPreviewId)/download") {
|
||||
let player = AVPlayer(url: url)
|
||||
player.play()
|
||||
self.mediaState = .video(player)
|
||||
} else if let url = URL(string: "https://white-nights.krbl.ru/services/content/media/\(sight.preview_media)/download") {
|
||||
self.mediaState = .image(url)
|
||||
} else {
|
||||
self.mediaState = .error
|
||||
}
|
||||
} else {
|
||||
do {
|
||||
let mediaItems = try await fetchJSON(from: "https://white-nights.krbl.ru/services/content/article/\(article.id)/media", type: [ArticleMedia].self)
|
||||
if let firstMedia = mediaItems.first,
|
||||
let url = URL(string: "https://white-nights.krbl.ru/services/content/media/\(firstMedia.id)/download") {
|
||||
self.mediaState = .image(url)
|
||||
} else {
|
||||
self.mediaState = .error
|
||||
}
|
||||
} catch {
|
||||
print("Ошибка загрузки медиа для статьи '\(article.heading)': \(error)")
|
||||
self.mediaState = .error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchJSON<T: Decodable>(from urlString: String, type: T.Type) async throws -> T {
|
||||
guard let url = URL(string: urlString) else {
|
||||
throw URLError(.badURL)
|
||||
}
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
return try JSONDecoder().decode(T.self, from: data)
|
||||
}
|
||||
}
|
11
WhiteNights/Widgets/VisualEffectBlur.swift
Normal file
@ -0,0 +1,11 @@
|
||||
import SwiftUI
|
||||
|
||||
struct VisualEffectBlur: UIViewRepresentable {
|
||||
var blurStyle: UIBlurEffect.Style
|
||||
|
||||
func makeUIView(context: Context) -> UIVisualEffectView {
|
||||
UIVisualEffectView(effect: UIBlurEffect(style: blurStyle))
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIVisualEffectView, context: Context) { }
|
||||
}
|
257
WhiteNights/Widgets/WeatherView.swift
Normal file
@ -0,0 +1,257 @@
|
||||
import SwiftUI
|
||||
|
||||
private let WEATHER_STATUS_MAP: [String: String] = [
|
||||
"Rain": "дождливо",
|
||||
"Clouds": "облачно",
|
||||
"Clear": "солнечно",
|
||||
"Thunderstorm": "гроза",
|
||||
"Snow": "снег",
|
||||
"Drizzle": "мелкий дождь",
|
||||
"Fog": "туман"
|
||||
]
|
||||
|
||||
struct FormattedWeather {
|
||||
let temperature: Int
|
||||
let status: String
|
||||
let precipitation: Int?
|
||||
let windSpeed: Double?
|
||||
let dayOfWeek: String?
|
||||
}
|
||||
|
||||
struct WeatherView: View {
|
||||
@State private var todayWeather: FormattedWeather?
|
||||
@State private var forecast: [FormattedWeather] = []
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
if let today = todayWeather {
|
||||
// ЛЕВЫЙ СТОЛБЕЦ: Иконка, температура и статус
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Image(getWeatherIconName(for: today.status))
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 36, height: 36)
|
||||
.padding(.bottom, 5)
|
||||
|
||||
Text("\(today.temperature)°")
|
||||
.font(.system(size: 30, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text(today.status)
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
// ПРАВЫЙ СТОЛБЕЦ: Прогноз с иконками
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
// Прогноз на 3 дня
|
||||
ForEach(forecast.prefix(3).indices, id: \.self) { index in
|
||||
let day = forecast[index]
|
||||
HStack(spacing: 5) {
|
||||
Image(getWeatherIconName(for: day.status))
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 14, height: 14)
|
||||
|
||||
if let dayName = day.dayOfWeek {
|
||||
Text(dayName)
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
Text("\(day.temperature)°")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
}
|
||||
|
||||
// Влажность и скорость ветра
|
||||
if let precipitation = today.precipitation {
|
||||
HStack(spacing: 5) {
|
||||
Image("det_humidity")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 14, height: 14)
|
||||
Text("\(precipitation)%")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
if let windSpeed = today.windSpeed {
|
||||
HStack(spacing: 5) {
|
||||
Image("det_wind_speed")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 14, height: 14)
|
||||
Text("\(Int(windSpeed)) м/с")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("Загрузка погоды...")
|
||||
.foregroundColor(.white)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.background(
|
||||
ZStack {
|
||||
Color(hex: 0x806C59)
|
||||
LinearGradient(
|
||||
stops: [
|
||||
.init(color: .white.opacity(0.0), location: 0.0871),
|
||||
.init(color: .white.opacity(0.16), location: 0.6969)
|
||||
],
|
||||
startPoint: .bottomLeading,
|
||||
endPoint: .topTrailing
|
||||
)
|
||||
}
|
||||
)
|
||||
.cornerRadius(25)
|
||||
.shadow(color: Color.black.opacity(0.10), radius: 8, x: 0, y: 2)
|
||||
.task {
|
||||
await fetchAndFormatWeather()
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchAndFormatWeather() async {
|
||||
let lat = 59.938784
|
||||
let lng = 30.314997
|
||||
|
||||
guard let url = URL(string: "https://white-nights.krbl.ru/services/weather") else { return }
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let parameters: [String: Any] = [
|
||||
"coordinates": ["latitude": lat, "longitude": lng]
|
||||
]
|
||||
|
||||
do {
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
|
||||
// 💡 Отладка: Печатаем полученные данные для проверки
|
||||
// print("Received Data (String): \(String(data: data, encoding: .utf8) ?? "N/A")")
|
||||
|
||||
let weatherResponse = try JSONDecoder().decode(WeatherResponse.self, from: data)
|
||||
|
||||
formatWeatherData(data: weatherResponse)
|
||||
|
||||
} catch {
|
||||
print("Ошибка загрузки погоды: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func formatWeatherData(data: WeatherResponse) {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) // UTC
|
||||
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // Добавление POSIX-локали для надежного парсинга
|
||||
|
||||
var calendar = Calendar.current
|
||||
calendar.timeZone = TimeZone(secondsFromGMT: 0)! // UTC
|
||||
|
||||
// 1. Фильтруем все записи, которые соответствуют 12:00 UTC
|
||||
let middayForecast = data.forecast.filter { item in
|
||||
guard let date = dateFormatter.date(from: item.date) else {
|
||||
// 💡 Отладка: если здесь вы увидите сообщения, значит, формат даты API не совпадает с форматом dateFormatter
|
||||
// print("Парсинг даты провален для: \(item.date)")
|
||||
return false
|
||||
}
|
||||
return calendar.component(.hour, from: date) == 12
|
||||
}
|
||||
|
||||
// 💡 Отладка: Проверяем, сколько записей найдено
|
||||
// print("Найдено записей на 12:00 UTC: \(middayForecast.count)")
|
||||
|
||||
// Сегодня
|
||||
if let today = data.currentWeather {
|
||||
self.todayWeather = FormattedWeather(
|
||||
temperature: Int(today.temperatureCelsius.rounded()),
|
||||
status: WEATHER_STATUS_MAP[today.description] ?? today.description,
|
||||
precipitation: today.humidity,
|
||||
windSpeed: today.windSpeed,
|
||||
dayOfWeek: nil
|
||||
)
|
||||
}
|
||||
|
||||
// 🚀 ИСПРАВЛЕНИЕ: Прогноз на 3 дня. Берем первые 3 доступные записи с 12:00.
|
||||
var formattedForecast: [FormattedWeather] = []
|
||||
|
||||
// Перебираем первые 3 записи с 12:00 UTC
|
||||
for item in middayForecast.prefix(3) {
|
||||
guard let date = dateFormatter.date(from: item.date) else { continue }
|
||||
|
||||
let averageTemp = (item.minTemperatureCelsius + item.maxTemperatureCelsius) / 2
|
||||
let dayOfWeekString = getDayOfWeek(from: date)
|
||||
|
||||
// 💡 Отладка: Печатаем найденный прогноз
|
||||
// print("Прогноз найден: \(dayOfWeekString) - \(Int(averageTemp.rounded()))°")
|
||||
|
||||
formattedForecast.append(FormattedWeather(
|
||||
temperature: Int(averageTemp.rounded()),
|
||||
status: WEATHER_STATUS_MAP[item.description] ?? item.description,
|
||||
precipitation: item.humidity,
|
||||
windSpeed: item.windSpeed,
|
||||
dayOfWeek: dayOfWeekString
|
||||
))
|
||||
}
|
||||
|
||||
// Если данных меньше 3, заполняем оставшиеся места нулями (N/A)
|
||||
let daysToFill = 3 - formattedForecast.count
|
||||
if daysToFill > 0 {
|
||||
let baseDate = Date()
|
||||
|
||||
for i in 1...daysToFill {
|
||||
guard let nextDayForNA = Calendar.current.date(byAdding: .day, value: formattedForecast.count + i, to: baseDate) else { continue }
|
||||
|
||||
let dayOfWeekString = getDayOfWeek(from: nextDayForNA)
|
||||
|
||||
formattedForecast.append(FormattedWeather(
|
||||
temperature: 0,
|
||||
status: "N/A",
|
||||
precipitation: nil,
|
||||
windSpeed: nil,
|
||||
dayOfWeek: dayOfWeekString
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
self.forecast = formattedForecast
|
||||
}
|
||||
|
||||
private func getDayOfWeek(from date: Date) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "ru_RU")
|
||||
formatter.dateFormat = "E"
|
||||
return formatter.string(from: date).capitalized
|
||||
}
|
||||
|
||||
private func getWeatherIconName(for status: String) -> String {
|
||||
let normalizedStatus = status.lowercased()
|
||||
|
||||
switch normalizedStatus {
|
||||
case "солнечно":
|
||||
return "cond_sunny"
|
||||
case "облачно":
|
||||
return "cond_cloudy"
|
||||
case "дождливо", "мелкий дождь":
|
||||
return "cond_rainy"
|
||||
case "снег":
|
||||
return "cond_snowy"
|
||||
case "гроза":
|
||||
return "cond_thunder"
|
||||
case "туман":
|
||||
return "det_humidity"
|
||||
default:
|
||||
return "cond_sunny"
|
||||
}
|
||||
}
|
||||
}
|