Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8593dfa33 | ||
|
|
86f6db3e55 | ||
|
|
8aaf31f1a1 | ||
|
|
ac29fd7114 | ||
|
|
05b86bf314 | ||
|
|
32ca4e121a | ||
|
|
e3642b3f1b | ||
|
|
e46198f87a | ||
|
|
fc0f7e2b50 | ||
|
|
d1cb2e28a6 | ||
|
|
cb3778a76f | ||
|
|
425099955b | ||
|
|
a54465cb1e | ||
|
|
c61a046b98 | ||
|
|
af811a95ad | ||
|
|
670ae76c73 | ||
|
|
6820a18691 | ||
|
|
196959ba36 | ||
|
|
6ff3dfb7ae | ||
|
|
b1cdbfe013 | ||
|
|
9339a22a7d | ||
|
|
2a174e5880 | ||
|
|
bdd885a082 | ||
|
|
d2561272c0 | ||
|
|
9297cd28f9 | ||
|
|
f38ed74865 | ||
|
|
16b1eab206 | ||
|
|
702d82ebb1 | ||
|
|
36d6b3ece7 | ||
|
|
6db26c55a8 | ||
|
|
2203fc9089 | ||
|
|
d002920567 | ||
|
|
4b209dcd72 | ||
|
|
080f36e690 | ||
|
|
b011616175 | ||
|
|
1e68e2a282 | ||
|
|
b3a5b8f3ae | ||
|
|
c9111fac0c | ||
|
|
643ddbc813 | ||
|
|
f4fffa3d70 | ||
|
|
c8636a22f1 | ||
|
|
8cbac16382 | ||
|
|
57ac574bb9 | ||
|
|
a845b54f0f | ||
|
|
00c40161c9 | ||
|
|
c372d4a522 | ||
|
|
89c070875c | ||
|
|
f671f4f76c |
@@ -387,6 +387,10 @@
|
|||||||
405C20942D07FC6300804C0B /* UpdatePlaylistRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405C20922D07FC6300804C0B /* UpdatePlaylistRequest.swift */; };
|
405C20942D07FC6300804C0B /* UpdatePlaylistRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405C20922D07FC6300804C0B /* UpdatePlaylistRequest.swift */; };
|
||||||
405C3FB32D78AB7900A1551A /* AppsFlyerLib-Static in Frameworks */ = {isa = PBXBuildFile; productRef = 405C3FB22D78AB7900A1551A /* AppsFlyerLib-Static */; };
|
405C3FB32D78AB7900A1551A /* AppsFlyerLib-Static in Frameworks */ = {isa = PBXBuildFile; productRef = 405C3FB22D78AB7900A1551A /* AppsFlyerLib-Static */; };
|
||||||
405C3FB52D78AB8900A1551A /* AppsFlyerLib-Static in Frameworks */ = {isa = PBXBuildFile; productRef = 405C3FB42D78AB8900A1551A /* AppsFlyerLib-Static */; };
|
405C3FB52D78AB8900A1551A /* AppsFlyerLib-Static in Frameworks */ = {isa = PBXBuildFile; productRef = 405C3FB42D78AB8900A1551A /* AppsFlyerLib-Static */; };
|
||||||
|
405EC8BB303D97F30037B269 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 405EC8BA303D97F30037B269 /* Lottie */; };
|
||||||
|
405EC8BD303D98100037B269 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 405EC8BC303D98100037B269 /* Lottie */; };
|
||||||
|
405EC8BF303D99D20037B269 /* pull-to-refresh.json in Resources */ = {isa = PBXBuildFile; fileRef = 405EC8BE303D99D20037B269 /* pull-to-refresh.json */; };
|
||||||
|
405EC8C0303D99D20037B269 /* pull-to-refresh.json in Resources */ = {isa = PBXBuildFile; fileRef = 405EC8BE303D99D20037B269 /* pull-to-refresh.json */; };
|
||||||
405F791B2D2B84D70037DE3A /* AuditionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405F791A2D2B84D70037DE3A /* AuditionItemView.swift */; };
|
405F791B2D2B84D70037DE3A /* AuditionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405F791A2D2B84D70037DE3A /* AuditionItemView.swift */; };
|
||||||
405F791C2D2B84D70037DE3A /* AuditionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405F791A2D2B84D70037DE3A /* AuditionItemView.swift */; };
|
405F791C2D2B84D70037DE3A /* AuditionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405F791A2D2B84D70037DE3A /* AuditionItemView.swift */; };
|
||||||
405F791F2D2BA3140037DE3A /* AuditionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405F791E2D2BA3140037DE3A /* AuditionDetailView.swift */; };
|
405F791F2D2BA3140037DE3A /* AuditionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405F791E2D2BA3140037DE3A /* AuditionDetailView.swift */; };
|
||||||
@@ -1077,8 +1081,6 @@
|
|||||||
40B28AB12A839E59008BE463 /* DetailNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B28AAF2A839E59008BE463 /* DetailNavigationBar.swift */; };
|
40B28AB12A839E59008BE463 /* DetailNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B28AAF2A839E59008BE463 /* DetailNavigationBar.swift */; };
|
||||||
40B28AB42A839E9D008BE463 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B28AB32A839E9D008BE463 /* ImagePicker.swift */; };
|
40B28AB42A839E9D008BE463 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B28AB32A839E9D008BE463 /* ImagePicker.swift */; };
|
||||||
40B28AB52A839E9D008BE463 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B28AB32A839E9D008BE463 /* ImagePicker.swift */; };
|
40B28AB52A839E9D008BE463 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B28AB32A839E9D008BE463 /* ImagePicker.swift */; };
|
||||||
7A03C0022E41400100C0DE01 /* ImageCropGeometry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A03C0012E41400100C0DE01 /* ImageCropGeometry.swift */; };
|
|
||||||
7A03C0032E41400100C0DE01 /* ImageCropGeometry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A03C0012E41400100C0DE01 /* ImageCropGeometry.swift */; };
|
|
||||||
40B37EE82A90A32C000DE782 /* ProfileUpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B37EE72A90A32C000DE782 /* ProfileUpdateView.swift */; };
|
40B37EE82A90A32C000DE782 /* ProfileUpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B37EE72A90A32C000DE782 /* ProfileUpdateView.swift */; };
|
||||||
40B37EE92A90A32C000DE782 /* ProfileUpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B37EE72A90A32C000DE782 /* ProfileUpdateView.swift */; };
|
40B37EE92A90A32C000DE782 /* ProfileUpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B37EE72A90A32C000DE782 /* ProfileUpdateView.swift */; };
|
||||||
40B37EEC2A90A3CF000DE782 /* ProfileUpdateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B37EEB2A90A3CF000DE782 /* ProfileUpdateViewModel.swift */; };
|
40B37EEC2A90A3CF000DE782 /* ProfileUpdateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B37EEB2A90A3CF000DE782 /* ProfileUpdateViewModel.swift */; };
|
||||||
@@ -1630,6 +1632,8 @@
|
|||||||
72F06849269BC1F597650385 /* CreatorDetailDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7406AE69F7667FAA2E6DD23 /* CreatorDetailDialogView.swift */; };
|
72F06849269BC1F597650385 /* CreatorDetailDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7406AE69F7667FAA2E6DD23 /* CreatorDetailDialogView.swift */; };
|
||||||
7389BAA35C414A1A0C1FE616 /* BannerCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24266E32FC49D7C31610C2AD /* BannerCarousel.swift */; };
|
7389BAA35C414A1A0C1FE616 /* BannerCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24266E32FC49D7C31610C2AD /* BannerCarousel.swift */; };
|
||||||
753741799DEF278DC4C515AA /* KakaoSDKAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 8069DA64D0A280105DF612B6 /* KakaoSDKAuth */; };
|
753741799DEF278DC4C515AA /* KakaoSDKAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 8069DA64D0A280105DF612B6 /* KakaoSDKAuth */; };
|
||||||
|
7A03C0022E41400100C0DE01 /* ImageCropGeometry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A03C0012E41400100C0DE01 /* ImageCropGeometry.swift */; };
|
||||||
|
7A03C0032E41400100C0DE01 /* ImageCropGeometry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A03C0012E41400100C0DE01 /* ImageCropGeometry.swift */; };
|
||||||
7A1DDAAE227BF176BDEF1CB9 /* TextTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E61608194B084224DB6B78B5 /* TextTabBar.swift */; };
|
7A1DDAAE227BF176BDEF1CB9 /* TextTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E61608194B084224DB6B78B5 /* TextTabBar.swift */; };
|
||||||
7AB453CE38CA0B3D612E9467 /* AudioContentCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E92246F5C1254CBED72AD9AD /* AudioContentCard.swift */; };
|
7AB453CE38CA0B3D612E9467 /* AudioContentCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E92246F5C1254CBED72AD9AD /* AudioContentCard.swift */; };
|
||||||
7B2EE8B4132668F0267B10CB /* UserProfileChannelDonationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 214C32BF44DF384FD6925E0B /* UserProfileChannelDonationView.swift */; };
|
7B2EE8B4132668F0267B10CB /* UserProfileChannelDonationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 214C32BF44DF384FD6925E0B /* UserProfileChannelDonationView.swift */; };
|
||||||
@@ -1668,6 +1672,10 @@
|
|||||||
A3BDD0D3E14CD72B022AF6A7 /* FollowButtonImageAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC033E531A9420363D017CE /* FollowButtonImageAsset.swift */; };
|
A3BDD0D3E14CD72B022AF6A7 /* FollowButtonImageAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC033E531A9420363D017CE /* FollowButtonImageAsset.swift */; };
|
||||||
A3EB81FD727C63FE034048B9 /* Spacing.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B18C00DE256DCC53030630 /* Spacing.swift */; };
|
A3EB81FD727C63FE034048B9 /* Spacing.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B18C00DE256DCC53030630 /* Spacing.swift */; };
|
||||||
A4EB669DFFFB64681E045EF3 /* MainHomeRecommendationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76506E56D017236824FE9B8F /* MainHomeRecommendationViewModel.swift */; };
|
A4EB669DFFFB64681E045EF3 /* MainHomeRecommendationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76506E56D017236824FE9B8F /* MainHomeRecommendationViewModel.swift */; };
|
||||||
|
A4F100000000000000000001 /* LottieRefreshIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4F100000000000000000005 /* LottieRefreshIndicator.swift */; };
|
||||||
|
A4F100000000000000000002 /* LottieRefreshIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4F100000000000000000005 /* LottieRefreshIndicator.swift */; };
|
||||||
|
A4F100000000000000000003 /* LottieRefreshableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4F100000000000000000006 /* LottieRefreshableScrollView.swift */; };
|
||||||
|
A4F100000000000000000004 /* LottieRefreshableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4F100000000000000000006 /* LottieRefreshableScrollView.swift */; };
|
||||||
A840136779D6CC95F94CB681 /* HomeTitleBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAA81E8424A575A0F8546D31 /* HomeTitleBar.swift */; };
|
A840136779D6CC95F94CB681 /* HomeTitleBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAA81E8424A575A0F8546D31 /* HomeTitleBar.swift */; };
|
||||||
ACD71056E97E13E48941ABBB /* MainPlaceholderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7900EA9FD1150BABE64CF1B3 /* MainPlaceholderTabView.swift */; };
|
ACD71056E97E13E48941ABBB /* MainPlaceholderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7900EA9FD1150BABE64CF1B3 /* MainPlaceholderTabView.swift */; };
|
||||||
AD6424211AA898794BBE8470 /* SectionTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16C12DFB83FE164B38CC066 /* SectionTitle.swift */; };
|
AD6424211AA898794BBE8470 /* SectionTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16C12DFB83FE164B38CC066 /* SectionTitle.swift */; };
|
||||||
@@ -2311,6 +2319,7 @@
|
|||||||
405C208C2D07FBC400804C0B /* ContentPlaylistModifyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentPlaylistModifyView.swift; sourceTree = "<group>"; };
|
405C208C2D07FBC400804C0B /* ContentPlaylistModifyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentPlaylistModifyView.swift; sourceTree = "<group>"; };
|
||||||
405C208F2D07FBEC00804C0B /* ContentPlaylistModifyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentPlaylistModifyViewModel.swift; sourceTree = "<group>"; };
|
405C208F2D07FBEC00804C0B /* ContentPlaylistModifyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentPlaylistModifyViewModel.swift; sourceTree = "<group>"; };
|
||||||
405C20922D07FC6300804C0B /* UpdatePlaylistRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePlaylistRequest.swift; sourceTree = "<group>"; };
|
405C20922D07FC6300804C0B /* UpdatePlaylistRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePlaylistRequest.swift; sourceTree = "<group>"; };
|
||||||
|
405EC8BE303D99D20037B269 /* pull-to-refresh.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "pull-to-refresh.json"; sourceTree = "<group>"; };
|
||||||
405F791A2D2B84D70037DE3A /* AuditionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuditionItemView.swift; sourceTree = "<group>"; };
|
405F791A2D2B84D70037DE3A /* AuditionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuditionItemView.swift; sourceTree = "<group>"; };
|
||||||
405F791E2D2BA3140037DE3A /* AuditionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuditionDetailView.swift; sourceTree = "<group>"; };
|
405F791E2D2BA3140037DE3A /* AuditionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuditionDetailView.swift; sourceTree = "<group>"; };
|
||||||
405F79212D2BA3230037DE3A /* AuditionDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuditionDetailViewModel.swift; sourceTree = "<group>"; };
|
405F79212D2BA3230037DE3A /* AuditionDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuditionDetailViewModel.swift; sourceTree = "<group>"; };
|
||||||
@@ -2646,7 +2655,6 @@
|
|||||||
40B28AAC2A839DE4008BE463 /* BaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseView.swift; sourceTree = "<group>"; };
|
40B28AAC2A839DE4008BE463 /* BaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseView.swift; sourceTree = "<group>"; };
|
||||||
40B28AAF2A839E59008BE463 /* DetailNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailNavigationBar.swift; sourceTree = "<group>"; };
|
40B28AAF2A839E59008BE463 /* DetailNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailNavigationBar.swift; sourceTree = "<group>"; };
|
||||||
40B28AB32A839E9D008BE463 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
|
40B28AB32A839E9D008BE463 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
|
||||||
7A03C0012E41400100C0DE01 /* ImageCropGeometry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCropGeometry.swift; sourceTree = "<group>"; };
|
|
||||||
40B37EE72A90A32C000DE782 /* ProfileUpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileUpdateView.swift; sourceTree = "<group>"; };
|
40B37EE72A90A32C000DE782 /* ProfileUpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileUpdateView.swift; sourceTree = "<group>"; };
|
||||||
40B37EEB2A90A3CF000DE782 /* ProfileUpdateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileUpdateViewModel.swift; sourceTree = "<group>"; };
|
40B37EEB2A90A3CF000DE782 /* ProfileUpdateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileUpdateViewModel.swift; sourceTree = "<group>"; };
|
||||||
40B37EF02A90A484000DE782 /* ModifyPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifyPasswordView.swift; sourceTree = "<group>"; };
|
40B37EF02A90A484000DE782 /* ModifyPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifyPasswordView.swift; sourceTree = "<group>"; };
|
||||||
@@ -2928,6 +2936,7 @@
|
|||||||
74990A0798E64A97237DD113 /* CapsuleTabBar.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CapsuleTabBar.swift; sourceTree = "<group>"; };
|
74990A0798E64A97237DD113 /* CapsuleTabBar.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CapsuleTabBar.swift; sourceTree = "<group>"; };
|
||||||
76506E56D017236824FE9B8F /* MainHomeRecommendationViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainHomeRecommendationViewModel.swift; sourceTree = "<group>"; };
|
76506E56D017236824FE9B8F /* MainHomeRecommendationViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainHomeRecommendationViewModel.swift; sourceTree = "<group>"; };
|
||||||
7900EA9FD1150BABE64CF1B3 /* MainPlaceholderTabView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainPlaceholderTabView.swift; sourceTree = "<group>"; };
|
7900EA9FD1150BABE64CF1B3 /* MainPlaceholderTabView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainPlaceholderTabView.swift; sourceTree = "<group>"; };
|
||||||
|
7A03C0012E41400100C0DE01 /* ImageCropGeometry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCropGeometry.swift; sourceTree = "<group>"; };
|
||||||
7C21D69F717F8D80644F6AEE /* PushNotificationListView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PushNotificationListView.swift; sourceTree = "<group>"; };
|
7C21D69F717F8D80644F6AEE /* PushNotificationListView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PushNotificationListView.swift; sourceTree = "<group>"; };
|
||||||
7C22ACAC456B3BCB2D9C568D /* AcceptLanguageDebugLogger.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AcceptLanguageDebugLogger.swift; sourceTree = "<group>"; };
|
7C22ACAC456B3BCB2D9C568D /* AcceptLanguageDebugLogger.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AcceptLanguageDebugLogger.swift; sourceTree = "<group>"; };
|
||||||
826D626B7714E5C28ED31074 /* I18n.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = I18n.swift; sourceTree = "<group>"; };
|
826D626B7714E5C28ED31074 /* I18n.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = I18n.swift; sourceTree = "<group>"; };
|
||||||
@@ -2937,6 +2946,8 @@
|
|||||||
959B90DBB4BB5911AB10C442 /* Pods-SodaLive-dev.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SodaLive-dev.debug.xcconfig"; path = "Target Support Files/Pods-SodaLive-dev/Pods-SodaLive-dev.debug.xcconfig"; sourceTree = "<group>"; };
|
959B90DBB4BB5911AB10C442 /* Pods-SodaLive-dev.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SodaLive-dev.debug.xcconfig"; path = "Target Support Files/Pods-SodaLive-dev/Pods-SodaLive-dev.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
9C7923ABAD25E9CC4840EC94 /* CreatorChannelFanTalkTabView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorChannelFanTalkTabView.swift; sourceTree = "<group>"; };
|
9C7923ABAD25E9CC4840EC94 /* CreatorChannelFanTalkTabView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorChannelFanTalkTabView.swift; sourceTree = "<group>"; };
|
||||||
A4AAC943F77B6FE089696909 /* UpdateContentPreferenceResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UpdateContentPreferenceResponse.swift; sourceTree = "<group>"; };
|
A4AAC943F77B6FE089696909 /* UpdateContentPreferenceResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UpdateContentPreferenceResponse.swift; sourceTree = "<group>"; };
|
||||||
|
A4F100000000000000000005 /* LottieRefreshIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieRefreshIndicator.swift; sourceTree = "<group>"; };
|
||||||
|
A4F100000000000000000006 /* LottieRefreshableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieRefreshableScrollView.swift; sourceTree = "<group>"; };
|
||||||
A575789542808C0C87AC6D82 /* AiCharacterCard.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AiCharacterCard.swift; sourceTree = "<group>"; };
|
A575789542808C0C87AC6D82 /* AiCharacterCard.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AiCharacterCard.swift; sourceTree = "<group>"; };
|
||||||
AB63E7E31B75FF2E6F1C9073 /* LanguageSettingsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LanguageSettingsView.swift; sourceTree = "<group>"; };
|
AB63E7E31B75FF2E6F1C9073 /* LanguageSettingsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LanguageSettingsView.swift; sourceTree = "<group>"; };
|
||||||
B108032F3DC7110CBFD04130 /* Pods-SodaLive-dev.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SodaLive-dev.release.xcconfig"; path = "Target Support Files/Pods-SodaLive-dev/Pods-SodaLive-dev.release.xcconfig"; sourceTree = "<group>"; };
|
B108032F3DC7110CBFD04130 /* Pods-SodaLive-dev.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SodaLive-dev.release.xcconfig"; path = "Target Support Files/Pods-SodaLive-dev/Pods-SodaLive-dev.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
@@ -3141,6 +3152,7 @@
|
|||||||
753741799DEF278DC4C515AA /* KakaoSDKAuth in Frameworks */,
|
753741799DEF278DC4C515AA /* KakaoSDKAuth in Frameworks */,
|
||||||
94E04DD7ABB0BBF450352E21 /* KakaoSDKUser in Frameworks */,
|
94E04DD7ABB0BBF450352E21 /* KakaoSDKUser in Frameworks */,
|
||||||
DD202E48D61F1CE8108AD164 /* LineSDK in Frameworks */,
|
DD202E48D61F1CE8108AD164 /* LineSDK in Frameworks */,
|
||||||
|
405EC8BB303D97F30037B269 /* Lottie in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -3171,6 +3183,7 @@
|
|||||||
3F829933ED2755E9F0BBC5FE /* KakaoSDKAuth in Frameworks */,
|
3F829933ED2755E9F0BBC5FE /* KakaoSDKAuth in Frameworks */,
|
||||||
C8B170409F057CA2B8501362 /* KakaoSDKUser in Frameworks */,
|
C8B170409F057CA2B8501362 /* KakaoSDKUser in Frameworks */,
|
||||||
A1465C799E10028802BD2A68 /* LineSDK in Frameworks */,
|
A1465C799E10028802BD2A68 /* LineSDK in Frameworks */,
|
||||||
|
405EC8BD303D98100037B269 /* Lottie in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -5481,6 +5494,7 @@
|
|||||||
40FE0E962A83622D00252293 /* Resources */ = {
|
40FE0E962A83622D00252293 /* Resources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
405EC8BE303D99D20037B269 /* pull-to-refresh.json */,
|
||||||
402830D62E8ABF2E001A6577 /* payverse_starter.html */,
|
402830D62E8ABF2E001A6577 /* payverse_starter.html */,
|
||||||
402830D72E8ABF2E001A6577 /* payverse_starter_debug.html */,
|
402830D72E8ABF2E001A6577 /* payverse_starter_debug.html */,
|
||||||
40FE0EAD2A83642C00252293 /* Debug */,
|
40FE0EAD2A83642C00252293 /* Debug */,
|
||||||
@@ -5642,6 +5656,7 @@
|
|||||||
F7F1421DBAB56E2F6891625E /* DefaultTitleBar.swift */,
|
F7F1421DBAB56E2F6891625E /* DefaultTitleBar.swift */,
|
||||||
DAA81E8424A575A0F8546D31 /* HomeTitleBar.swift */,
|
DAA81E8424A575A0F8546D31 /* HomeTitleBar.swift */,
|
||||||
25C5D0734F54C0ED7B7BD462 /* Modal */,
|
25C5D0734F54C0ED7B7BD462 /* Modal */,
|
||||||
|
A4F100000000000000000007 /* Refresh */,
|
||||||
D16C12DFB83FE164B38CC066 /* SectionTitle.swift */,
|
D16C12DFB83FE164B38CC066 /* SectionTitle.swift */,
|
||||||
0FD267162A809D893E2E3B3F /* Text */,
|
0FD267162A809D893E2E3B3F /* Text */,
|
||||||
E61608194B084224DB6B78B5 /* TextTabBar.swift */,
|
E61608194B084224DB6B78B5 /* TextTabBar.swift */,
|
||||||
@@ -5650,6 +5665,15 @@
|
|||||||
path = Component;
|
path = Component;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
A4F100000000000000000007 /* Refresh */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A4F100000000000000000005 /* LottieRefreshIndicator.swift */,
|
||||||
|
A4F100000000000000000006 /* LottieRefreshableScrollView.swift */,
|
||||||
|
);
|
||||||
|
path = Refresh;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
AA4588B91C0F73344C3DC4A4 /* FanTalk */ = {
|
AA4588B91C0F73344C3DC4A4 /* FanTalk */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -6413,6 +6437,7 @@
|
|||||||
8069DA64D0A280105DF612B6 /* KakaoSDKAuth */,
|
8069DA64D0A280105DF612B6 /* KakaoSDKAuth */,
|
||||||
061FD682D7FE09325C4B460A /* KakaoSDKUser */,
|
061FD682D7FE09325C4B460A /* KakaoSDKUser */,
|
||||||
6DBA0C325ACEC3FCDB22490D /* LineSDK */,
|
6DBA0C325ACEC3FCDB22490D /* LineSDK */,
|
||||||
|
405EC8BA303D97F30037B269 /* Lottie */,
|
||||||
);
|
);
|
||||||
productName = SodaLive;
|
productName = SodaLive;
|
||||||
productReference = 40FE0E842A8361DF00252293 /* SodaLive.app */;
|
productReference = 40FE0E842A8361DF00252293 /* SodaLive.app */;
|
||||||
@@ -6456,6 +6481,7 @@
|
|||||||
E9CD5F29965B148AB360C353 /* KakaoSDKAuth */,
|
E9CD5F29965B148AB360C353 /* KakaoSDKAuth */,
|
||||||
927A707B5035B0A77C0F5EA6 /* KakaoSDKUser */,
|
927A707B5035B0A77C0F5EA6 /* KakaoSDKUser */,
|
||||||
867D78B93CBC5738B7D43CEA /* LineSDK */,
|
867D78B93CBC5738B7D43CEA /* LineSDK */,
|
||||||
|
405EC8BC303D98100037B269 /* Lottie */,
|
||||||
);
|
);
|
||||||
productName = SodaLive;
|
productName = SodaLive;
|
||||||
productReference = 40FE0EAC2A8362A200252293 /* SodaLive-dev.app */;
|
productReference = 40FE0EAC2A8362A200252293 /* SodaLive-dev.app */;
|
||||||
@@ -6504,6 +6530,7 @@
|
|||||||
404DCB3D2D9A6FCC000935C5 /* XCRemoteSwiftPackageReference "objectbox-swift-spm" */,
|
404DCB3D2D9A6FCC000935C5 /* XCRemoteSwiftPackageReference "objectbox-swift-spm" */,
|
||||||
534A307BA769D6BC2D6343CB /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */,
|
534A307BA769D6BC2D6343CB /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */,
|
||||||
7D0E5FAF7AD538D1C2FED682 /* XCRemoteSwiftPackageReference "line-sdk-ios-swift" */,
|
7D0E5FAF7AD538D1C2FED682 /* XCRemoteSwiftPackageReference "line-sdk-ios-swift" */,
|
||||||
|
405EC8B9303D97F30037B269 /* XCRemoteSwiftPackageReference "lottie-spm" */,
|
||||||
);
|
);
|
||||||
productRefGroup = 40FE0E852A8361DF00252293 /* Products */;
|
productRefGroup = 40FE0E852A8361DF00252293 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@@ -6533,6 +6560,7 @@
|
|||||||
40B640742E20BA6300D0E4E4 /* Pretendard-Light.otf in Resources */,
|
40B640742E20BA6300D0E4E4 /* Pretendard-Light.otf in Resources */,
|
||||||
40B640752E20BA6300D0E4E4 /* Pretendard-Bold.otf in Resources */,
|
40B640752E20BA6300D0E4E4 /* Pretendard-Bold.otf in Resources */,
|
||||||
402830D82E8ABF2E001A6577 /* payverse_starter_debug.html in Resources */,
|
402830D82E8ABF2E001A6577 /* payverse_starter_debug.html in Resources */,
|
||||||
|
405EC8BF303D99D20037B269 /* pull-to-refresh.json in Resources */,
|
||||||
402830D92E8ABF2E001A6577 /* payverse_starter.html in Resources */,
|
402830D92E8ABF2E001A6577 /* payverse_starter.html in Resources */,
|
||||||
40FE0E8C2A8361E100252293 /* Assets.xcassets in Resources */,
|
40FE0E8C2A8361E100252293 /* Assets.xcassets in Resources */,
|
||||||
40A5BA882A8B38CC004A4635 /* GoogleService-Info.plist in Resources */,
|
40A5BA882A8B38CC004A4635 /* GoogleService-Info.plist in Resources */,
|
||||||
@@ -6556,6 +6584,7 @@
|
|||||||
40B640702E20BA6300D0E4E4 /* Pretendard-Light.otf in Resources */,
|
40B640702E20BA6300D0E4E4 /* Pretendard-Light.otf in Resources */,
|
||||||
40B640712E20BA6300D0E4E4 /* Pretendard-Bold.otf in Resources */,
|
40B640712E20BA6300D0E4E4 /* Pretendard-Bold.otf in Resources */,
|
||||||
402830DA2E8ABF2E001A6577 /* payverse_starter_debug.html in Resources */,
|
402830DA2E8ABF2E001A6577 /* payverse_starter_debug.html in Resources */,
|
||||||
|
405EC8C0303D99D20037B269 /* pull-to-refresh.json in Resources */,
|
||||||
402830DB2E8ABF2E001A6577 /* payverse_starter.html in Resources */,
|
402830DB2E8ABF2E001A6577 /* payverse_starter.html in Resources */,
|
||||||
40FE0EA62A8362A200252293 /* Assets.xcassets in Resources */,
|
40FE0EA62A8362A200252293 /* Assets.xcassets in Resources */,
|
||||||
40728D522D79780A00D2C580 /* GoogleService-Info.plist in Resources */,
|
40728D522D79780A00D2C580 /* GoogleService-Info.plist in Resources */,
|
||||||
@@ -6722,6 +6751,8 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
A4F100000000000000000001 /* LottieRefreshIndicator.swift in Sources */,
|
||||||
|
A4F100000000000000000003 /* LottieRefreshableScrollView.swift in Sources */,
|
||||||
400EDE592B31D2D7008E37B4 /* CreatorCommunityApi.swift in Sources */,
|
400EDE592B31D2D7008E37B4 /* CreatorCommunityApi.swift in Sources */,
|
||||||
CC0101012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift in Sources */,
|
CC0101012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift in Sources */,
|
||||||
CC0101032FF0100000A5BBA3 /* CreatorChannelHomeApi.swift in Sources */,
|
CC0101032FF0100000A5BBA3 /* CreatorChannelHomeApi.swift in Sources */,
|
||||||
@@ -7822,6 +7853,8 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
A4F100000000000000000002 /* LottieRefreshIndicator.swift in Sources */,
|
||||||
|
A4F100000000000000000004 /* LottieRefreshableScrollView.swift in Sources */,
|
||||||
400EDE5A2B31D2D7008E37B4 /* CreatorCommunityApi.swift in Sources */,
|
400EDE5A2B31D2D7008E37B4 /* CreatorCommunityApi.swift in Sources */,
|
||||||
409E35092B0759DE00CCB985 /* MoyaProviderExtension.swift in Sources */,
|
409E35092B0759DE00CCB985 /* MoyaProviderExtension.swift in Sources */,
|
||||||
40DDBD782B2B4FD700A2E8DA /* CreatorCommunityMoreItemView.swift in Sources */,
|
40DDBD782B2B4FD700A2E8DA /* CreatorCommunityMoreItemView.swift in Sources */,
|
||||||
@@ -8912,13 +8945,13 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = SodaLive/SodaLive.entitlements;
|
CODE_SIGN_ENTITLEMENTS = SodaLive/SodaLive.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"SodaLive/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"SodaLive/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = 3A8QGPMRXM;
|
DEVELOPMENT_TEAM = 3A8QGPMRXM;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = SodaLive/Resources/Info.plist;
|
INFOPLIST_FILE = SodaLive/Resources/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "보이스온";
|
INFOPLIST_KEY_CFBundleDisplayName = "소다라이브";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
|
||||||
INFOPLIST_KEY_NSCameraUsageDescription = "프로필 이미지 / 라이브 커버이미지 촬영을 위해 카메라 접근 권한이 필요합니다.";
|
INFOPLIST_KEY_NSCameraUsageDescription = "프로필 이미지 / 라이브 커버이미지 촬영을 위해 카메라 접근 권한이 필요합니다.";
|
||||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "라이브 / 음성메시지 / 오디션 녹음을 위해 마이크 권한이 필요합니다.";
|
INFOPLIST_KEY_NSMicrophoneUsageDescription = "라이브 / 음성메시지 / 오디션 녹음을 위해 마이크 권한이 필요합니다.";
|
||||||
@@ -8934,7 +8967,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.56.0;
|
MARKETING_VERSION = 1.56.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = kr.co.vividnext.sodalive;
|
PRODUCT_BUNDLE_IDENTIFIER = kr.co.vividnext.sodalive;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -8953,13 +8986,13 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = SodaLive/SodaLive.entitlements;
|
CODE_SIGN_ENTITLEMENTS = SodaLive/SodaLive.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"SodaLive/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"SodaLive/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = 3A8QGPMRXM;
|
DEVELOPMENT_TEAM = 3A8QGPMRXM;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = SodaLive/Resources/Info.plist;
|
INFOPLIST_FILE = SodaLive/Resources/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "보이스온";
|
INFOPLIST_KEY_CFBundleDisplayName = "소다라이브";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
|
||||||
INFOPLIST_KEY_NSCameraUsageDescription = "프로필 이미지 / 라이브 커버이미지 촬영을 위해 카메라 접근 권한이 필요합니다.";
|
INFOPLIST_KEY_NSCameraUsageDescription = "프로필 이미지 / 라이브 커버이미지 촬영을 위해 카메라 접근 권한이 필요합니다.";
|
||||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "라이브 / 음성메시지 / 오디션 녹음을 위해 마이크 권한이 필요합니다.";
|
INFOPLIST_KEY_NSMicrophoneUsageDescription = "라이브 / 음성메시지 / 오디션 녹음을 위해 마이크 권한이 필요합니다.";
|
||||||
@@ -8975,7 +9008,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.56.0;
|
MARKETING_VERSION = 1.56.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = kr.co.vividnext.sodalive;
|
PRODUCT_BUNDLE_IDENTIFIER = kr.co.vividnext.sodalive;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -8999,7 +9032,7 @@
|
|||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "SodaLive/Resources/Debug/SodaLive-dev-Info.plist";
|
INFOPLIST_FILE = "SodaLive/Resources/Debug/SodaLive-dev-Info.plist";
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "보이스온-테스트";
|
INFOPLIST_KEY_CFBundleDisplayName = "소다라이브-테스트";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
|
||||||
INFOPLIST_KEY_NSCameraUsageDescription = "프로필 이미지 / 라이브 커버이미지 촬영을 위해 카메라 접근 권한이 필요합니다.";
|
INFOPLIST_KEY_NSCameraUsageDescription = "프로필 이미지 / 라이브 커버이미지 촬영을 위해 카메라 접근 권한이 필요합니다.";
|
||||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "라이브 / 음성메시지 / 오디션 녹음을 위해 마이크 권한이 필요합니다.";
|
INFOPLIST_KEY_NSMicrophoneUsageDescription = "라이브 / 음성메시지 / 오디션 녹음을 위해 마이크 권한이 필요합니다.";
|
||||||
@@ -9039,7 +9072,7 @@
|
|||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "SodaLive/Resources/Debug/SodaLive-dev-Info.plist";
|
INFOPLIST_FILE = "SodaLive/Resources/Debug/SodaLive-dev-Info.plist";
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "보이스온-테스트";
|
INFOPLIST_KEY_CFBundleDisplayName = "소다라이브-테스트";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
|
||||||
INFOPLIST_KEY_NSCameraUsageDescription = "프로필 이미지 / 라이브 커버이미지 촬영을 위해 카메라 접근 권한이 필요합니다.";
|
INFOPLIST_KEY_NSCameraUsageDescription = "프로필 이미지 / 라이브 커버이미지 촬영을 위해 카메라 접근 권한이 필요합니다.";
|
||||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "라이브 / 음성메시지 / 오디션 녹음을 위해 마이크 권한이 필요합니다.";
|
INFOPLIST_KEY_NSMicrophoneUsageDescription = "라이브 / 음성메시지 / 오디션 녹음을 위해 마이크 권한이 필요합니다.";
|
||||||
@@ -9130,6 +9163,14 @@
|
|||||||
minimumVersion = 6.16.1;
|
minimumVersion = 6.16.1;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
405EC8B9303D97F30037B269 /* XCRemoteSwiftPackageReference "lottie-spm" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/airbnb/lottie-spm.git";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 4.6.1;
|
||||||
|
};
|
||||||
|
};
|
||||||
406AEAA12D6D9CCE00D1AEFE /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */ = {
|
406AEAA12D6D9CCE00D1AEFE /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/facebook/facebook-ios-sdk.git";
|
repositoryURL = "https://github.com/facebook/facebook-ios-sdk.git";
|
||||||
@@ -9279,6 +9320,16 @@
|
|||||||
package = 405C3FB12D78AB7900A1551A /* XCRemoteSwiftPackageReference "AppsFlyerFramework-Static" */;
|
package = 405C3FB12D78AB7900A1551A /* XCRemoteSwiftPackageReference "AppsFlyerFramework-Static" */;
|
||||||
productName = "AppsFlyerLib-Static";
|
productName = "AppsFlyerLib-Static";
|
||||||
};
|
};
|
||||||
|
405EC8BA303D97F30037B269 /* Lottie */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 405EC8B9303D97F30037B269 /* XCRemoteSwiftPackageReference "lottie-spm" */;
|
||||||
|
productName = Lottie;
|
||||||
|
};
|
||||||
|
405EC8BC303D98100037B269 /* Lottie */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 405EC8B9303D97F30037B269 /* XCRemoteSwiftPackageReference "lottie-spm" */;
|
||||||
|
productName = Lottie;
|
||||||
|
};
|
||||||
406AEAA22D6D9CCE00D1AEFE /* FacebookCore */ = {
|
406AEAA22D6D9CCE00D1AEFE /* FacebookCore */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 406AEAA12D6D9CCE00D1AEFE /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */;
|
package = 406AEAA12D6D9CCE00D1AEFE /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash": "1f28da3687662a2a9efe60ffc2ca2499be411b5b0a1e07f72559059c40728121",
|
"originHash" : "879f32da6c663e9c4a413e005f26682b94e0dfa4995d4a5b8119800465a10127",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "abseil-cpp-binary",
|
"identity" : "abseil-cpp-binary",
|
||||||
@@ -163,6 +163,15 @@
|
|||||||
"version" : "5.14.0"
|
"version" : "5.14.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "lottie-spm",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/airbnb/lottie-spm.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "0aff16288f8dc1e3dab35cbf5cf45979673fd3df",
|
||||||
|
"version" : "4.6.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "moya",
|
"identity" : "moya",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 124 KiB |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "1024x1024.jpg",
|
"filename" : "iOS_1024_appstore.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"platform" : "ios",
|
"platform" : "ios",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
|
|||||||
|
After Width: | Height: | Size: 14 KiB |
@@ -1,15 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "img_apply_creator.png",
|
"filename" : "img_apply_creator_kr.png",
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "3x"
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 286 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "img_apply_creator_ja.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 288 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "img_introduce_sodalive_kr.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 185 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "img_introduce_sodalive_ja.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 188 KiB |
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "img_introduce_voiceon.png",
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4.1 MiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "ic_content_tag_original.png",
|
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
@@ -10,6 +9,7 @@
|
|||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "ic_content_tag_original.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "3x"
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 549 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 446 B After Width: | Height: | Size: 346 B |
|
Before Width: | Height: | Size: 408 B After Width: | Height: | Size: 394 B |
|
Before Width: | Height: | Size: 556 B After Width: | Height: | Size: 505 B |
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
|
"filename" : "ic_new_following_white.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
@@ -9,7 +10,6 @@
|
|||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "splash_bg.png",
|
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "3x"
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
|
After Width: | Height: | Size: 381 B |
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "ic_series_original.png",
|
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
@@ -10,6 +9,7 @@
|
|||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "ic_series_original.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "3x"
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 603 B After Width: | Height: | Size: 847 B |
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "img_new_only.png",
|
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
@@ -10,6 +9,7 @@
|
|||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "img_new_only.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "3x"
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 482 B After Width: | Height: | Size: 1.2 KiB |
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "img_text_logo.png",
|
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
@@ -10,6 +9,7 @@
|
|||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "img_text_logo.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "3x"
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
|
After Width: | Height: | Size: 3.8 KiB |
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "img_text_logo_v2.png",
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -5,7 +5,7 @@
|
|||||||
Created by klaus on 12/5/25.
|
Created by klaus on 12/5/25.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
"CFBundleDisplayName" = "VoiceOn-Test";
|
"CFBundleDisplayName" = "SodaLive-Test";
|
||||||
"NSPhotoLibraryUsageDescription" = "Allow access to your photo library to select photos.";
|
"NSPhotoLibraryUsageDescription" = "Allow access to your photo library to select photos.";
|
||||||
"NSMicrophoneUsageDescription" = "Allow access to your microphone to record audio.";
|
"NSMicrophoneUsageDescription" = "Allow access to your microphone to record audio.";
|
||||||
"NSUserTrackingUsageDescription" = "Allow tracking to deliver personalized ads.";
|
"NSUserTrackingUsageDescription" = "Allow tracking to deliver personalized ads.";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
Created by klaus on 12/5/25.
|
Created by klaus on 12/5/25.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
"CFBundleDisplayName" = "ボイスオン-テスト";
|
"CFBundleDisplayName" = "ソーダライブ-テスト";
|
||||||
"NSPhotoLibraryUsageDescription" = "写真を選択するために写真ライブラリへのアクセスが必要です。";
|
"NSPhotoLibraryUsageDescription" = "写真を選択するために写真ライブラリへのアクセスが必要です。";
|
||||||
"NSMicrophoneUsageDescription" = "音声を録音するためにマイクへのアクセスが必要です。";
|
"NSMicrophoneUsageDescription" = "音声を録音するためにマイクへのアクセスが必要です。";
|
||||||
"NSUserTrackingUsageDescription" = "パーソナライズされた広告のためにトラッキングを許可してください。";
|
"NSUserTrackingUsageDescription" = "パーソナライズされた広告のためにトラッキングを許可してください。";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
Created by klaus on 12/5/25.
|
Created by klaus on 12/5/25.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
"CFBundleDisplayName" = "보이스온-테스트";
|
"CFBundleDisplayName" = "소다라이브-테스트";
|
||||||
"NSPhotoLibraryUsageDescription" = "사진을 선택하려면 사진 라이브러리 접근이 필요합니다.";
|
"NSPhotoLibraryUsageDescription" = "사진을 선택하려면 사진 라이브러리 접근이 필요합니다.";
|
||||||
"NSMicrophoneUsageDescription" = "음성 녹음을 위해 마이크 접근이 필요합니다.";
|
"NSMicrophoneUsageDescription" = "음성 녹음을 위해 마이크 접근이 필요합니다.";
|
||||||
"NSUserTrackingUsageDescription" = "맞춤형 광고 제공을 위해 추적을 허용해 주세요.";
|
"NSUserTrackingUsageDescription" = "맞춤형 광고 제공을 위해 추적을 허용해 주세요.";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
Created by klaus on 12/5/25.
|
Created by klaus on 12/5/25.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
"CFBundleDisplayName" = "VoiceOn";
|
"CFBundleDisplayName" = "SodaLive";
|
||||||
"NSPhotoLibraryUsageDescription" = "Allow access to your photo library to select photos.";
|
"NSPhotoLibraryUsageDescription" = "Allow access to your photo library to select photos.";
|
||||||
"NSMicrophoneUsageDescription" = "Allow access to your microphone to record audio.";
|
"NSMicrophoneUsageDescription" = "Allow access to your microphone to record audio.";
|
||||||
"NSUserTrackingUsageDescription" = "Allow tracking to deliver personalized ads.";
|
"NSUserTrackingUsageDescription" = "Allow tracking to deliver personalized ads.";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
Created by klaus on 12/5/25.
|
Created by klaus on 12/5/25.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
"CFBundleDisplayName" = "ボイスオン";
|
"CFBundleDisplayName" = "ソーダライブ";
|
||||||
"NSPhotoLibraryUsageDescription" = "写真を選択するために写真ライブラリへのアクセスが必要です。";
|
"NSPhotoLibraryUsageDescription" = "写真を選択するために写真ライブラリへのアクセスが必要です。";
|
||||||
"NSMicrophoneUsageDescription" = "音声を録音するためにマイクへのアクセスが必要です。";
|
"NSMicrophoneUsageDescription" = "音声を録音するためにマイクへのアクセスが必要です。";
|
||||||
"NSUserTrackingUsageDescription" = "パーソナライズされた広告のためにトラッキングを許可してください。";
|
"NSUserTrackingUsageDescription" = "パーソナライズされた広告のためにトラッキングを許可してください。";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
Created by klaus on 12/5/25.
|
Created by klaus on 12/5/25.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
"CFBundleDisplayName" = "보이스온";
|
"CFBundleDisplayName" = "소다라이브";
|
||||||
"NSPhotoLibraryUsageDescription" = "사진을 선택하려면 사진 라이브러리 접근이 필요합니다.";
|
"NSPhotoLibraryUsageDescription" = "사진을 선택하려면 사진 라이브러리 접근이 필요합니다.";
|
||||||
"NSMicrophoneUsageDescription" = "음성 녹음을 위해 마이크 접근이 필요합니다.";
|
"NSMicrophoneUsageDescription" = "음성 녹음을 위해 마이크 접근이 필요합니다.";
|
||||||
"NSUserTrackingUsageDescription" = "맞춤형 광고 제공을 위해 추적을 허용해 주세요.";
|
"NSUserTrackingUsageDescription" = "맞춤형 광고 제공을 위해 추적을 허용해 주세요.";
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
{
|
||||||
|
"v": "5.9.0",
|
||||||
|
"fr": 30,
|
||||||
|
"ip": 0,
|
||||||
|
"op": 45,
|
||||||
|
"w": 150,
|
||||||
|
"h": 150,
|
||||||
|
"nm": "loading",
|
||||||
|
"ddd": 0,
|
||||||
|
"assets": [],
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"ddd": 0,
|
||||||
|
"ind": 1,
|
||||||
|
"ty": 4,
|
||||||
|
"nm": "loading",
|
||||||
|
"sr": 1,
|
||||||
|
"ks": {
|
||||||
|
"o": {
|
||||||
|
"a": 0,
|
||||||
|
"k": 100,
|
||||||
|
"ix": 11
|
||||||
|
},
|
||||||
|
"r": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"i": { "x": [0.667], "y": [1] },
|
||||||
|
"o": { "x": [0.333], "y": [0] },
|
||||||
|
"t": 0,
|
||||||
|
"s": [-90]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 45,
|
||||||
|
"s": [270]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ix": 10
|
||||||
|
},
|
||||||
|
"p": {
|
||||||
|
"a": 0,
|
||||||
|
"k": [75, 75, 0],
|
||||||
|
"ix": 2,
|
||||||
|
"l": 2
|
||||||
|
},
|
||||||
|
"a": {
|
||||||
|
"a": 0,
|
||||||
|
"k": [0, 0, 0],
|
||||||
|
"ix": 1,
|
||||||
|
"l": 2
|
||||||
|
},
|
||||||
|
"s": {
|
||||||
|
"a": 0,
|
||||||
|
"k": [100, 100, 100],
|
||||||
|
"ix": 6,
|
||||||
|
"l": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ao": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"d": 1,
|
||||||
|
"ty": "el",
|
||||||
|
"s": {
|
||||||
|
"a": 0,
|
||||||
|
"k": [100, 100],
|
||||||
|
"ix": 2
|
||||||
|
},
|
||||||
|
"p": {
|
||||||
|
"a": 0,
|
||||||
|
"k": [0, 0],
|
||||||
|
"ix": 3
|
||||||
|
},
|
||||||
|
"nm": "타원 패스 1",
|
||||||
|
"mn": "ADBE Vector Shape - Ellipse",
|
||||||
|
"hd": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "st",
|
||||||
|
"c": {
|
||||||
|
"a": 0,
|
||||||
|
"k": [1, 1, 1, 1],
|
||||||
|
"ix": 3
|
||||||
|
},
|
||||||
|
"o": {
|
||||||
|
"a": 0,
|
||||||
|
"k": 100,
|
||||||
|
"ix": 4
|
||||||
|
},
|
||||||
|
"w": {
|
||||||
|
"a": 0,
|
||||||
|
"k": 20,
|
||||||
|
"ix": 5
|
||||||
|
},
|
||||||
|
"lc": 2,
|
||||||
|
"lj": 1,
|
||||||
|
"ml": 4,
|
||||||
|
"bm": 0,
|
||||||
|
"nm": "선 1",
|
||||||
|
"mn": "ADBE Vector Graphic - Stroke",
|
||||||
|
"hd": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "tr",
|
||||||
|
"p": {
|
||||||
|
"a": 0,
|
||||||
|
"k": [0, 0],
|
||||||
|
"ix": 2
|
||||||
|
},
|
||||||
|
"a": {
|
||||||
|
"a": 0,
|
||||||
|
"k": [0, 0],
|
||||||
|
"ix": 1
|
||||||
|
},
|
||||||
|
"s": {
|
||||||
|
"a": 0,
|
||||||
|
"k": [100, 100],
|
||||||
|
"ix": 3
|
||||||
|
},
|
||||||
|
"r": {
|
||||||
|
"a": 0,
|
||||||
|
"k": 0,
|
||||||
|
"ix": 6
|
||||||
|
},
|
||||||
|
"o": {
|
||||||
|
"a": 0,
|
||||||
|
"k": 100,
|
||||||
|
"ix": 7
|
||||||
|
},
|
||||||
|
"sk": {
|
||||||
|
"a": 0,
|
||||||
|
"k": 0,
|
||||||
|
"ix": 4
|
||||||
|
},
|
||||||
|
"sa": {
|
||||||
|
"a": 0,
|
||||||
|
"k": 0,
|
||||||
|
"ix": 5
|
||||||
|
},
|
||||||
|
"nm": "변형"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nm": "타원 1",
|
||||||
|
"np": 3,
|
||||||
|
"cix": 2,
|
||||||
|
"bm": 0,
|
||||||
|
"ix": 1,
|
||||||
|
"mn": "ADBE Vector Group",
|
||||||
|
"hd": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "tm",
|
||||||
|
"s": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"i": { "x": [0.667], "y": [1] },
|
||||||
|
"o": { "x": [0.333], "y": [0] },
|
||||||
|
"t": 19.78,
|
||||||
|
"s": [3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 45,
|
||||||
|
"s": [97]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ix": 1
|
||||||
|
},
|
||||||
|
"e": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"i": { "x": [0.667], "y": [1] },
|
||||||
|
"o": { "x": [0.333], "y": [0] },
|
||||||
|
"t": 0,
|
||||||
|
"s": [3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"i": { "x": [0.833], "y": [1] },
|
||||||
|
"o": { "x": [0.167], "y": [0] },
|
||||||
|
"t": 26.372,
|
||||||
|
"s": [100]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 45,
|
||||||
|
"s": [97]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ix": 2
|
||||||
|
},
|
||||||
|
"o": {
|
||||||
|
"a": 0,
|
||||||
|
"k": 0,
|
||||||
|
"ix": 3
|
||||||
|
},
|
||||||
|
"m": 1,
|
||||||
|
"ix": 2,
|
||||||
|
"nm": "패스 다듬어 자르기 1",
|
||||||
|
"mn": "ADBE Vector Filter - Trim",
|
||||||
|
"hd": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ip": 0,
|
||||||
|
"op": 45,
|
||||||
|
"st": 0,
|
||||||
|
"bm": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"markers": []
|
||||||
|
}
|
||||||
@@ -95,7 +95,7 @@ struct OriginalWorkDetailHeaderView: View {
|
|||||||
contentType: "웹소설",
|
contentType: "웹소설",
|
||||||
category: "로맨스",
|
category: "로맨스",
|
||||||
isAdult: true,
|
isAdult: true,
|
||||||
description: "작품설명입니다.보이스온의 오픈월드 캐릭터톡은 청소년 보호를 위해 본인인증한성인만 이용이 가능합니다.캐릭터톡 서비스를 이용하시려면 본인인증을 하고 이용해주세요.",
|
description: "작품설명입니다.소다라이브의 오픈월드 캐릭터톡은 청소년 보호를 위해 본인인증한성인만 이용이 가능합니다.캐릭터톡 서비스를 이용하시려면 본인인증을 하고 이용해주세요.",
|
||||||
originalWork: nil,
|
originalWork: nil,
|
||||||
originalLink: "https://apple.com",
|
originalLink: "https://apple.com",
|
||||||
writer: nil,
|
writer: nil,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ struct SeriesListAllView: View {
|
|||||||
if isCompleted {
|
if isCompleted {
|
||||||
DetailNavigationBar(title: I18n.Series.completedSectionTitle)
|
DetailNavigationBar(title: I18n.Series.completedSectionTitle)
|
||||||
} else if isOriginal {
|
} else if isOriginal {
|
||||||
DetailNavigationBar(title: I18n.Series.voiceOnOnlyTitle)
|
DetailNavigationBar(title: I18n.Series.sodaLiveOnlyTitle)
|
||||||
} else {
|
} else {
|
||||||
DetailNavigationBar(title: I18n.Series.viewAllByCreator(creatorNickname ?? ""))
|
DetailNavigationBar(title: I18n.Series.viewAllByCreator(creatorNickname ?? ""))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,19 @@ struct FollowCreatorItemView: View {
|
|||||||
let creator: GetCreatorFollowingAllListItem
|
let creator: GetCreatorFollowingAllListItem
|
||||||
let onClickFollow: (Int) -> Void
|
let onClickFollow: (Int) -> Void
|
||||||
let showCreatorFollowNotifyDialog: (Int) -> Void
|
let showCreatorFollowNotifyDialog: (Int) -> Void
|
||||||
|
let onTapUnfollow: ((Int) -> Void)?
|
||||||
|
|
||||||
|
init(
|
||||||
|
creator: GetCreatorFollowingAllListItem,
|
||||||
|
onClickFollow: @escaping (Int) -> Void,
|
||||||
|
showCreatorFollowNotifyDialog: @escaping (Int) -> Void,
|
||||||
|
onTapUnfollow: ((Int) -> Void)? = nil
|
||||||
|
) {
|
||||||
|
self.creator = creator
|
||||||
|
self.onClickFollow = onClickFollow
|
||||||
|
self.showCreatorFollowNotifyDialog = showCreatorFollowNotifyDialog
|
||||||
|
self.onTapUnfollow = onTapUnfollow
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 13.3) {
|
VStack(spacing: 13.3) {
|
||||||
@@ -36,6 +49,28 @@ struct FollowCreatorItemView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
followButton
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.foregroundColor(Color.gray59)
|
||||||
|
.frame(height: 0.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var followButton: some View {
|
||||||
|
if creator.isFollow, let onTapUnfollow {
|
||||||
|
Button(action: { onTapUnfollow(creator.creatorId) }) {
|
||||||
|
Image("ic_new_following_white")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
.frame(width: 36, height: 36)
|
||||||
|
.background(Color.gray800)
|
||||||
|
.clipShape(Circle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
} else {
|
||||||
let asset = FollowButtonImageAsset(
|
let asset = FollowButtonImageAsset(
|
||||||
type: creator.isFollow
|
type: creator.isFollow
|
||||||
? (creator.isNotify ? .following : .followingNoAlarm)
|
? (creator.isNotify ? .following : .followingNoAlarm)
|
||||||
@@ -50,10 +85,5 @@ struct FollowCreatorItemView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle()
|
|
||||||
.foregroundColor(Color.gray59)
|
|
||||||
.frame(height: 0.5)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,16 +11,17 @@ struct FollowCreatorView: View {
|
|||||||
|
|
||||||
@StateObject var viewModel = FollowCreatorViewModel()
|
@StateObject var viewModel = FollowCreatorViewModel()
|
||||||
|
|
||||||
@State private var isShowFollowNotifyDialog: Bool = false
|
@State private var isShowUnfollowConfirmDialog: Bool = false
|
||||||
@State private var creatorId: Int = 0
|
@State private var creatorId: Int = 0
|
||||||
@State private var selectedItemIndex: Int = 0
|
@State private var selectedItemIndex: Int = 0
|
||||||
|
@State private var creatorName: String = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
DetailNavigationBar(title: I18n.Follow.followingListTitle)
|
DetailNavigationBar(title: I18n.Follow.followingListTitle)
|
||||||
|
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 4) {
|
||||||
Text(I18n.Follow.totalPrefix)
|
Text(I18n.Follow.totalPrefix)
|
||||||
.appFont(size: 13.3, weight: .medium)
|
.appFont(size: 13.3, weight: .medium)
|
||||||
.foregroundColor(Color.grayee)
|
.foregroundColor(Color.grayee)
|
||||||
@@ -29,10 +30,6 @@ struct FollowCreatorView: View {
|
|||||||
.appFont(size: 13.3, weight: .medium)
|
.appFont(size: 13.3, weight: .medium)
|
||||||
.foregroundColor(Color.mainRed3)
|
.foregroundColor(Color.mainRed3)
|
||||||
|
|
||||||
Text(I18n.Follow.personUnit)
|
|
||||||
.appFont(size: 13.3, weight: .medium)
|
|
||||||
.foregroundColor(Color.grayee)
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 13.3)
|
.padding(.horizontal, 13.3)
|
||||||
@@ -52,7 +49,12 @@ struct FollowCreatorView: View {
|
|||||||
showCreatorFollowNotifyDialog: {
|
showCreatorFollowNotifyDialog: {
|
||||||
creatorId = $0
|
creatorId = $0
|
||||||
selectedItemIndex = index
|
selectedItemIndex = index
|
||||||
isShowFollowNotifyDialog = true
|
},
|
||||||
|
onTapUnfollow: {
|
||||||
|
creatorId = $0
|
||||||
|
creatorName = creator.nickname
|
||||||
|
selectedItemIndex = index
|
||||||
|
isShowUnfollowConfirmDialog = true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
@@ -81,41 +83,44 @@ struct FollowCreatorView: View {
|
|||||||
}
|
}
|
||||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
|
|
||||||
if isShowFollowNotifyDialog {
|
if isShowUnfollowConfirmDialog {
|
||||||
CreatorFollowNotifyDialog(
|
unfollowConfirmDialog
|
||||||
isShowing: $isShowFollowNotifyDialog,
|
}
|
||||||
onClickNotifyAll: {
|
}
|
||||||
viewModel.creatorFollow(
|
}
|
||||||
creatorId: creatorId,
|
|
||||||
index: selectedItemIndex,
|
private var unfollowConfirmDialog: some View {
|
||||||
follow: true,
|
SodaV2ActionModal(
|
||||||
notify: true
|
title: I18n.MemberChannel.unfollowActionModalTitle,
|
||||||
|
message: I18n.MemberChannel.unfollowActionModalMessage(creatorName),
|
||||||
|
button1: SodaV2ActionModalButton(
|
||||||
|
label: I18n.MemberChannel.unfollowActionModalConfirm,
|
||||||
|
action: confirmUnfollow
|
||||||
|
),
|
||||||
|
button2: SodaV2ActionModalButton(
|
||||||
|
label: I18n.MemberChannel.unfollowActionModalCancel,
|
||||||
|
action: dismissUnfollowConfirmDialog
|
||||||
|
),
|
||||||
|
onDimmedTap: dismissUnfollowConfirmDialog
|
||||||
)
|
)
|
||||||
creatorId = 0
|
}
|
||||||
selectedItemIndex = -1
|
|
||||||
},
|
private func confirmUnfollow() {
|
||||||
onClickNotifyNone: {
|
let targetCreatorId = creatorId
|
||||||
|
let targetIndex = selectedItemIndex
|
||||||
|
dismissUnfollowConfirmDialog()
|
||||||
viewModel.creatorFollow(
|
viewModel.creatorFollow(
|
||||||
creatorId: creatorId,
|
creatorId: targetCreatorId,
|
||||||
index: selectedItemIndex,
|
index: targetIndex,
|
||||||
follow: true,
|
|
||||||
notify: false
|
|
||||||
)
|
|
||||||
creatorId = 0
|
|
||||||
selectedItemIndex = -1
|
|
||||||
},
|
|
||||||
onClickUnFollow: {
|
|
||||||
viewModel.creatorFollow(
|
|
||||||
creatorId: creatorId,
|
|
||||||
index: selectedItemIndex,
|
|
||||||
follow: false,
|
follow: false,
|
||||||
notify: false
|
notify: false
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func dismissUnfollowConfirmDialog() {
|
||||||
|
isShowUnfollowConfirmDialog = false
|
||||||
creatorId = 0
|
creatorId = 0
|
||||||
selectedItemIndex = -1
|
selectedItemIndex = -1
|
||||||
}
|
creatorName = ""
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ struct HomeTabView: View {
|
|||||||
if !viewModel.originalAudioDramaList.isEmpty {
|
if !viewModel.originalAudioDramaList.isEmpty {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
Text(I18n.Home.onlyOnVoiceOnSectionTitle)
|
Text(I18n.Home.onlyOnSodaLiveSectionTitle)
|
||||||
.appFont(size: 24, weight: .bold)
|
.appFont(size: 24, weight: .bold)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
|
||||||
|
|||||||
@@ -90,9 +90,9 @@ enum I18n {
|
|||||||
|
|
||||||
static var dialogDescription: String {
|
static var dialogDescription: String {
|
||||||
pick(
|
pick(
|
||||||
ko: "보이스온의 오픈월드 캐릭터톡은\n청소년 보호를 위해 본인인증한\n성인만 이용이 가능합니다.\n캐릭터톡 서비스를 이용하시려면\n본인인증을 하고 이용해주세요.",
|
ko: "소다라이브의 오픈월드 캐릭터톡은\n청소년 보호를 위해 본인인증한\n성인만 이용이 가능합니다.\n캐릭터톡 서비스를 이용하시려면\n본인인증을 하고 이용해주세요.",
|
||||||
en: "VoiceOn Open World Character Talk is available only to adults who complete identity verification for youth protection.\nPlease complete identity verification to use Character Talk.",
|
en: "SodaLive Open World Character Talk is available only to adults who complete identity verification for youth protection.\nPlease complete identity verification to use Character Talk.",
|
||||||
ja: "VoiceOnオープンワールドキャラクタートークは、青少年保護のため本人認証を完了した成人のみ利用できます。\nキャラクタートークを利用するには本人認証を完了してください。"
|
ja: "SodaLiveオープンワールドキャラクタートークは、青少年保護のため本人認証を完了した成人のみ利用できます。\nキャラクタートークを利用するには本人認証を完了してください。"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,9 +164,9 @@ enum I18n {
|
|||||||
|
|
||||||
static var detailConversationGuideDescription1: String {
|
static var detailConversationGuideDescription1: String {
|
||||||
pick(
|
pick(
|
||||||
ko: "보이스온의 오픈월드 캐릭터톡은 대화의 자유도가 높아 대화에 참여하는 당신은 누구든 될 수 있습니다. 세계관 속 연관 캐릭터가 되어 대화를 하거나 완전히 새로운 인물이 되어 캐릭터와 당신만의 스토리를 만들어 갈 수 있습니다.",
|
ko: "소다라이브의 오픈월드 캐릭터톡은 대화의 자유도가 높아 대화에 참여하는 당신은 누구든 될 수 있습니다. 세계관 속 연관 캐릭터가 되어 대화를 하거나 완전히 새로운 인물이 되어 캐릭터와 당신만의 스토리를 만들어 갈 수 있습니다.",
|
||||||
en: "VoiceOn Open World Character Talk gives you high conversational freedom. You can become a related character in the world or a completely new persona and build your own story with the character.",
|
en: "SodaLive Open World Character Talk gives you high conversational freedom. You can become a related character in the world or a completely new persona and build your own story with the character.",
|
||||||
ja: "VoiceOnオープンワールドキャラクタートークは会話の自由度が高く、世界観の関連キャラクターとして会話したり、まったく新しい人物になってキャラクターとあなただけの物語を作れます。"
|
ja: "SodaLiveオープンワールドキャラクタートークは会話の自由度が高く、世界観の関連キャラクターとして会話したり、まったく新しい人物になってキャラクターとあなただけの物語を作れます。"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ enum I18n {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var detailChatButton: String {
|
static var detailChatButton: String {
|
||||||
pick(ko: "대화하기", en: "Start chat", ja: "会話する")
|
pick(ko: "대화하기", en: "Start chat", ja: "トークする")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var detailCollapse: String {
|
static var detailCollapse: String {
|
||||||
@@ -196,7 +196,7 @@ enum I18n {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var totalPrefix: String {
|
static var totalPrefix: String {
|
||||||
pick(ko: "전체", en: "Total", ja: "全体")
|
pick(ko: "전체", en: "Total", ja: "すべて")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var countUnit: String {
|
static var countUnit: String {
|
||||||
@@ -254,9 +254,9 @@ enum I18n {
|
|||||||
enum Room {
|
enum Room {
|
||||||
static var noticeForCharacter: String {
|
static var noticeForCharacter: String {
|
||||||
pick(
|
pick(
|
||||||
ko: "보이스온 AI캐릭터톡은 대화의 자유도가 높아 대화에 참여하는 당신은 누구든 될 수 있습니다.\n세계관 속 캐릭터로 대화를 하거나 새로운 인물로 캐릭터와 당신만의 스토리를 만들어보세요.\n※ AI캐릭터톡은 오픈베타 서비스 중이며, 캐릭터의 대화가 어색하거나 불완전할 수 있습니다.",
|
ko: "소다라이브 AI캐릭터톡은 대화의 자유도가 높아 대화에 참여하는 당신은 누구든 될 수 있습니다.\n세계관 속 캐릭터로 대화를 하거나 새로운 인물로 캐릭터와 당신만의 스토리를 만들어보세요.\n※ AI캐릭터톡은 오픈베타 서비스 중이며, 캐릭터의 대화가 어색하거나 불완전할 수 있습니다.",
|
||||||
en: "VoiceOn AI Character Talk offers a high degree of conversational freedom.\nTalk as a character in the world or as a new persona and create your own story with the character.\n※ AI Character Talk is in open beta, so responses may be awkward or incomplete.",
|
en: "SodaLive AI Character Talk offers a high degree of conversational freedom.\nTalk as a character in the world or as a new persona and create your own story with the character.\n※ AI Character Talk is in open beta, so responses may be awkward or incomplete.",
|
||||||
ja: "VoiceOn AIキャラクタートークは会話の自由度が高く、会話に参加するあなたは誰にでもなれます。\n世界観のキャラクターとして会話したり、新しい人物としてキャラクターとあなただけの物語を作ってみてください。\n※ AIキャラクタートークはオープンベータ中のため、会話が不自然または不完全な場合があります。"
|
ja: "SodaLive AIキャラクタートークは会話の自由度が高く、会話に参加するあなたは誰にでもなれます。\n世界観のキャラクターとして会話したり、新しい人物としてキャラクターとあなただけの物語を作ってみてください。\n※ AIキャラクタートークはオープンベータ中のため、会話が不自然または不完全な場合があります。"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,7 +505,7 @@ enum I18n {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var donation: String {
|
static var donation: String {
|
||||||
pick(ko: "후원", en: "Donate", ja: "後援")
|
pick(ko: "후원", en: "Donate", ja: "サポート")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var previewAlertMessage: String {
|
static var previewAlertMessage: String {
|
||||||
@@ -643,7 +643,7 @@ enum I18n {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum DonationDialog {
|
enum DonationDialog {
|
||||||
static var title: String { pick(ko: "후원하기", en: "Donate", ja: "後援する") }
|
static var title: String { pick(ko: "후원하기", en: "Donate", ja: "サポートする") }
|
||||||
static var charge: String { pick(ko: "충전", en: "Charge", ja: "チャージ") }
|
static var charge: String { pick(ko: "충전", en: "Charge", ja: "チャージ") }
|
||||||
static var minimumOneCanPlaceholder: String { pick(ko: "1캔 이상 입력하세요", en: "Enter 1 can or more", ja: "1can以上入力してください") }
|
static var minimumOneCanPlaceholder: String { pick(ko: "1캔 이상 입력하세요", en: "Enter 1 can or more", ja: "1can以上入力してください") }
|
||||||
static var minimumTenCanPlaceholder: String { pick(ko: "10캔 이상 입력하세요", en: "Enter 10 cans or more", ja: "10can以上入力してください") }
|
static var minimumTenCanPlaceholder: String { pick(ko: "10캔 이상 입력하세요", en: "Enter 10 cans or more", ja: "10can以上入力してください") }
|
||||||
@@ -658,7 +658,7 @@ enum I18n {
|
|||||||
ja: "一緒に送る\(jaPrefix)メッセージを入力(最大\(limit)文字)"
|
ja: "一緒に送る\(jaPrefix)メッセージを入力(最大\(limit)文字)"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
static var donateAction: String { pick(ko: "후원하기", en: "Donate", ja: "後援する") }
|
static var donateAction: String { pick(ko: "후원하기", en: "Donate", ja: "サポートする") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
enum ContentBox {
|
enum ContentBox {
|
||||||
@@ -734,7 +734,7 @@ enum I18n {
|
|||||||
|
|
||||||
enum Sort {
|
enum Sort {
|
||||||
static var newest: String {
|
static var newest: String {
|
||||||
pick(ko: "최신순", en: "Newest", ja: "新着順")
|
pick(ko: "최신순", en: "Newest", ja: "最新順")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var popularity: String {
|
static var popularity: String {
|
||||||
@@ -742,17 +742,17 @@ enum I18n {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var priceHigh: String {
|
static var priceHigh: String {
|
||||||
pick(ko: "높은 가격순", en: "Highest price", ja: "価格の高い順")
|
pick(ko: "높은 가격순", en: "Highest price", ja: "高い順")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var priceLow: String {
|
static var priceLow: String {
|
||||||
pick(ko: "낮은 가격순", en: "Lowest price", ja: "価格の低い順")
|
pick(ko: "낮은 가격순", en: "Lowest price", ja: "安い順")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Count {
|
enum Count {
|
||||||
static var totalPrefix: String {
|
static var totalPrefix: String {
|
||||||
pick(ko: "전체", en: "Total", ja: "全体")
|
pick(ko: "전체", en: "Total", ja: "すべて")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var countUnit: String {
|
static var countUnit: String {
|
||||||
@@ -762,7 +762,7 @@ enum I18n {
|
|||||||
|
|
||||||
enum Status {
|
enum Status {
|
||||||
static var owned: String {
|
static var owned: String {
|
||||||
pick(ko: "소장중", en: "Owned", ja: "所持中")
|
pick(ko: "소장중", en: "Owned", ja: "保有中")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var rented: String {
|
static var rented: String {
|
||||||
@@ -798,9 +798,9 @@ enum I18n {
|
|||||||
static var createNewAction: String { pick(ko: "+ 새 재생목록 만들기", en: "+ Create new playlist", ja: "+ 新しいプレイリストを作成") }
|
static var createNewAction: String { pick(ko: "+ 새 재생목록 만들기", en: "+ Create new playlist", ja: "+ 新しいプレイリストを作成") }
|
||||||
static var emptyTitle: String { pick(ko: "재생목록이 비어있습니다.", en: "Your playlist is empty.", ja: "プレイリストが空です。") }
|
static var emptyTitle: String { pick(ko: "재생목록이 비어있습니다.", en: "Your playlist is empty.", ja: "プレイリストが空です。") }
|
||||||
static var emptyDescription: String { pick(ko: "자주 듣는 콘텐츠를\n재생목록으로 만들어 보세요.", en: "Create a playlist with\nyour frequently listened content.", ja: "よく聴くコンテンツで\nプレイリストを作ってみましょう。") }
|
static var emptyDescription: String { pick(ko: "자주 듣는 콘텐츠를\n재생목록으로 만들어 보세요.", en: "Create a playlist with\nyour frequently listened content.", ja: "よく聴くコンテンツで\nプレイリストを作ってみましょう。") }
|
||||||
static var totalLabel: String { pick(ko: "전체", en: "Total", ja: "全体") }
|
static var totalLabel: String { pick(ko: "전체", en: "Total", ja: "すべて") }
|
||||||
static func totalCount(_ count: Int) -> String { pick(ko: "총 \(count)개", en: "Total \(count)", ja: "全\(count)件") }
|
static func totalCount(_ count: Int) -> String { pick(ko: "전체 \(count)개", en: "Total \(count)", ja: "すべて\(count)件") }
|
||||||
static func itemCount(_ count: Int) -> String { pick(ko: "총 \(count)개", en: "Total \(count)", ja: "全\(count)件") }
|
static func itemCount(_ count: Int) -> String { pick(ko: "전체 \(count)개", en: "Total \(count)", ja: "すべて\(count)件") }
|
||||||
static var createTitle: String { pick(ko: "새 재생목록 만들기", en: "Create new playlist", ja: "新しいプレイリストを作成") }
|
static var createTitle: String { pick(ko: "새 재생목록 만들기", en: "Create new playlist", ja: "新しいプレイリストを作成") }
|
||||||
static var createSave: String { pick(ko: "저장", en: "Save", ja: "保存") }
|
static var createSave: String { pick(ko: "저장", en: "Save", ja: "保存") }
|
||||||
static var modifyTitle: String { pick(ko: "재생목록 수정", en: "Edit playlist", ja: "プレイリストを編集") }
|
static var modifyTitle: String { pick(ko: "재생목록 수정", en: "Edit playlist", ja: "プレイリストを編集") }
|
||||||
@@ -954,15 +954,15 @@ enum I18n {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var defaultTitle: String {
|
static var defaultTitle: String {
|
||||||
pick(ko: "보이스온", en: "VoiceOn", ja: "ボイスオン")
|
pick(ko: "소다라이브", en: "SodaLive", ja: "ソーダライブ")
|
||||||
}
|
}
|
||||||
|
|
||||||
enum List {
|
enum List {
|
||||||
static var usageGuide: String {
|
static var usageGuide: String {
|
||||||
pick(
|
pick(
|
||||||
ko: "보이스온 오디션 이용방법",
|
ko: "소다라이브 오디션 이용방법",
|
||||||
en: "How to use VoiceOn Audition",
|
en: "How to use SodaLive Audition",
|
||||||
ja: "ボイスオンオーディション利用方法"
|
ja: "ソーダライブオーディション利用方法"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1036,9 +1036,9 @@ enum I18n {
|
|||||||
|
|
||||||
static var privacyAgreement: String {
|
static var privacyAgreement: String {
|
||||||
pick(
|
pick(
|
||||||
ko: "보이스온 오디오 드라마 오디션 합격시 개인 연락을 위한 개인 정보(연락처) 수집 및 활용에 동의합니다.\n오디션 지원자는 개인정보 수집 및 활용 동의에 거부할 권리가 있으며 비동의시 오디션 지원은 취소 됩니다.",
|
ko: "소다라이브 오디오 드라마 오디션 합격시 개인 연락을 위한 개인 정보(연락처) 수집 및 활용에 동의합니다.\n오디션 지원자는 개인정보 수집 및 활용 동의에 거부할 권리가 있으며 비동의시 오디션 지원은 취소 됩니다.",
|
||||||
en: "I agree to the collection and use of personal information (contact) for personal contact if selected for the VoiceOn audio drama audition.\nApplicants have the right to refuse consent to the collection and use of personal information, and if they do not consent, the audition application will be canceled.",
|
en: "I agree to the collection and use of personal information (contact) for personal contact if selected for the SodaLive audio drama audition.\nApplicants have the right to refuse consent to the collection and use of personal information, and if they do not consent, the audition application will be canceled.",
|
||||||
ja: "ボイスオンオーディオドラマオーディション合格時の個別連絡のため、個人情報(連絡先)の収集および利用に同意します。\n応募者は個人情報の収集および利用への同意を拒否する権利があり、同意しない場合はオーディション応募が取り消されます。"
|
ja: "ソーダライブオーディオドラマオーディション合格時の個別連絡のため、個人情報(連絡先)の収集および利用に同意します。\n応募者は個人情報の収集および利用への同意を拒否する権利があり、同意しない場合はオーディション応募が取り消されます。"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1175,7 +1175,7 @@ enum I18n {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var sortNewest: String {
|
static var sortNewest: String {
|
||||||
pick(ko: "최신순", en: "Newest", ja: "新着順")
|
pick(ko: "최신순", en: "Newest", ja: "最新順")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var sortLikes: String {
|
static var sortLikes: String {
|
||||||
@@ -1246,7 +1246,7 @@ enum I18n {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var reportAction: String {
|
static var reportAction: String {
|
||||||
pick(ko: "신고", en: "Report", ja: "報告する")
|
pick(ko: "신고", en: "Report", ja: "通報")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var cheersReasons: [String] {
|
static var cheersReasons: [String] {
|
||||||
@@ -1616,7 +1616,7 @@ enum I18n {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var all: String {
|
static var all: String {
|
||||||
pick(ko: "전체", en: "All", ja: "全体")
|
pick(ko: "전체", en: "All", ja: "すべて")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var maleOriented: String {
|
static var maleOriented: String {
|
||||||
@@ -1708,7 +1708,7 @@ enum I18n {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var totalPrefix: String {
|
static var totalPrefix: String {
|
||||||
pick(ko: "총", en: "Total", ja: "合計")
|
pick(ko: "전체", en: "Total", ja: "すべて")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var countUnit: String {
|
static var countUnit: String {
|
||||||
@@ -2041,7 +2041,7 @@ enum I18n {
|
|||||||
|
|
||||||
// 성별 제한
|
// 성별 제한
|
||||||
static var genderRestrictionTitle: String { pick(ko: "성별 제한", en: "Gender restriction", ja: "性別制限") }
|
static var genderRestrictionTitle: String { pick(ko: "성별 제한", en: "Gender restriction", ja: "性別制限") }
|
||||||
static var genderAll: String { pick(ko: "전체", en: "All", ja: "全体") }
|
static var genderAll: String { pick(ko: "전체", en: "All", ja: "すべて") }
|
||||||
static var genderMaleOnly: String { pick(ko: "남자만", en: "Male only", ja: "男性のみ") }
|
static var genderMaleOnly: String { pick(ko: "남자만", en: "Male only", ja: "男性のみ") }
|
||||||
static var genderFemaleOnly: String { pick(ko: "여자만", en: "Female only", ja: "女性のみ") }
|
static var genderFemaleOnly: String { pick(ko: "여자만", en: "Female only", ja: "女性のみ") }
|
||||||
|
|
||||||
@@ -2106,7 +2106,7 @@ enum I18n {
|
|||||||
static var menuSavedMessage: String { pick(ko: "저장되었습니다.", en: "Saved.", ja: "保存されました。") }
|
static var menuSavedMessage: String { pick(ko: "저장되었습니다.", en: "Saved.", ja: "保存されました。") }
|
||||||
|
|
||||||
static var editLiveTitle: String { pick(ko: "라이브 수정", en: "Edit live", ja: "ライブ編集") }
|
static var editLiveTitle: String { pick(ko: "라이브 수정", en: "Edit live", ja: "ライブ編集") }
|
||||||
static var editAction: String { pick(ko: "수정하기", en: "Edit", ja: "編集する") }
|
static var editAction: String { pick(ko: "수정하기", en: "Edit", ja: "編集") }
|
||||||
static var entryMessageLabel: String { pick(ko: "입장메시지", en: "Entry message", ja: "入場メッセージ") }
|
static var entryMessageLabel: String { pick(ko: "입장메시지", en: "Entry message", ja: "入場メッセージ") }
|
||||||
static var noChangesMessage: String { pick(ko: "변경사항이 없습니다.", en: "No changes.", ja: "変更事項がありません。") }
|
static var noChangesMessage: String { pick(ko: "변경사항이 없습니다.", en: "No changes.", ja: "変更事項がありません。") }
|
||||||
static var editSuccessMessage: String { pick(ko: "라이브 정보가 수정되었습니다.", en: "Live info has been updated.", ja: "ライブ情報を更新しました。") }
|
static var editSuccessMessage: String { pick(ko: "라이브 정보가 수정되었습니다.", en: "Live info has been updated.", ja: "ライブ情報を更新しました。") }
|
||||||
@@ -2125,8 +2125,7 @@ enum I18n {
|
|||||||
|
|
||||||
static var donationRankingTitle: String { pick(ko: "현재 라이브 후원랭킹", en: "Current live donation ranking", ja: "現在のライブ後援ランキング") }
|
static var donationRankingTitle: String { pick(ko: "현재 라이브 후원랭킹", en: "Current live donation ranking", ja: "現在のライブ後援ランキング") }
|
||||||
static var heartRankingTitle: String { pick(ko: "현재 라이브 하트랭킹", en: "Current live heart ranking", ja: "現在のライブハートランキング") }
|
static var heartRankingTitle: String { pick(ko: "현재 라이브 하트랭킹", en: "Current live heart ranking", ja: "現在のライブハートランキング") }
|
||||||
static var totalLabel: String { pick(ko: "전체", en: "Total", ja: "全体") }
|
static var totalLabel: String { pick(ko: "전체", en: "Total", ja: "すべて") }
|
||||||
static var peopleUnit: String { pick(ko: "명", en: "people", ja: "人") }
|
|
||||||
static var sumLabel: String { pick(ko: "합계", en: "Sum", ja: "合計") }
|
static var sumLabel: String { pick(ko: "합계", en: "Sum", ja: "合計") }
|
||||||
static var normalLabel: String { pick(ko: "일반", en: "Normal", ja: "一般") }
|
static var normalLabel: String { pick(ko: "일반", en: "Normal", ja: "一般") }
|
||||||
static var secretLabel: String { pick(ko: "비밀", en: "Secret", ja: "秘密") }
|
static var secretLabel: String { pick(ko: "비밀", en: "Secret", ja: "秘密") }
|
||||||
@@ -2317,16 +2316,16 @@ enum I18n {
|
|||||||
static var releasedStaffByHostMessage: String { pick(ko: "방장님이 스탭에서 해제했습니다.", en: "Host removed your staff role.", ja: "配信者があなたのスタッフ権限を解除しました。") }
|
static var releasedStaffByHostMessage: String { pick(ko: "방장님이 스탭에서 해제했습니다.", en: "Host removed your staff role.", ja: "配信者があなたのスタッフ権限を解除しました。") }
|
||||||
static func invitePrivateLiveMessage(_ nickname: String, _ shareUrl: String, _ password: String) -> String {
|
static func invitePrivateLiveMessage(_ nickname: String, _ shareUrl: String, _ password: String) -> String {
|
||||||
pick(
|
pick(
|
||||||
ko: "\(nickname)님이 귀하를 보이스온 비공개라이브에 초대하였습니다.\n※ 라이브 참여: \(shareUrl)\n(입장 비밀번호: \(password))",
|
ko: "\(nickname)님이 귀하를 소다라이브 비공개라이브에 초대하였습니다.\n※ 라이브 참여: \(shareUrl)\n(입장 비밀번호: \(password))",
|
||||||
en: "\(nickname) invited you to a private VoiceOn live.\n※ Join live: \(shareUrl)\n(Entry password: \(password))",
|
en: "\(nickname) invited you to a private SodaLive live.\n※ Join live: \(shareUrl)\n(Entry password: \(password))",
|
||||||
ja: "\(nickname)さんがあなたをVoiceOn非公開ライブに招待しました。\n※ ライブ参加: \(shareUrl)\n(入場パスワード: \(password))"
|
ja: "\(nickname)さんがあなたをSodaLive非公開ライブに招待しました。\n※ ライブ参加: \(shareUrl)\n(入場パスワード: \(password))"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
static func invitePublicLiveMessage(_ nickname: String, _ shareUrl: String) -> String {
|
static func invitePublicLiveMessage(_ nickname: String, _ shareUrl: String) -> String {
|
||||||
pick(
|
pick(
|
||||||
ko: "\(nickname)님이 귀하를 보이스온 공개라이브에 초대하였습니다.\n※ 라이브 참여: \(shareUrl)",
|
ko: "\(nickname)님이 귀하를 소다라이브 공개라이브에 초대하였습니다.\n※ 라이브 참여: \(shareUrl)",
|
||||||
en: "\(nickname) invited you to a public VoiceOn live.\n※ Join live: \(shareUrl)",
|
en: "\(nickname) invited you to a public SodaLive live.\n※ Join live: \(shareUrl)",
|
||||||
ja: "\(nickname)さんがあなたをVoiceOn公開ライブに招待しました。\n※ ライブ参加: \(shareUrl)"
|
ja: "\(nickname)さんがあなたをSodaLive公開ライブに招待しました。\n※ ライブ参加: \(shareUrl)"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2701,13 +2700,13 @@ enum I18n {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum Category {
|
enum Category {
|
||||||
static var all: String { pick(ko: "전체", en: "All", ja: "全") }
|
static var all: String { pick(ko: "전체", en: "All", ja: "すべて") }
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RankingSort {
|
enum RankingSort {
|
||||||
// 분석/지표 등
|
// 분석/지표 등
|
||||||
static var revenue: String { pick(ko: "매출", en: "Revenue", ja: "売上高") }
|
static var revenue: String { pick(ko: "매출", en: "Revenue", ja: "売上") }
|
||||||
static var salesVolume: String { pick(ko: "판매량", en: "Units", ja: "販売量") }
|
static var salesVolume: String { pick(ko: "판매량", en: "Units", ja: "販売数") }
|
||||||
static var comments: String { pick(ko: "댓글", en: "Comments", ja: "コメント") }
|
static var comments: String { pick(ko: "댓글", en: "Comments", ja: "コメント") }
|
||||||
static var likes: String { pick(ko: "좋아요", en: "Likes", ja: "いいね") }
|
static var likes: String { pick(ko: "좋아요", en: "Likes", ja: "いいね") }
|
||||||
}
|
}
|
||||||
@@ -2741,6 +2740,16 @@ enum I18n {
|
|||||||
ja: "\(nickname)さんのフォローを解除しますか?"
|
ja: "\(nickname)さんのフォローを解除しますか?"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
static var unfollowActionModalTitle: String { pick(ko: "팔로우 해제", en: "Unfollow", ja: "フォロー解除") }
|
||||||
|
static func unfollowActionModalMessage(_ nickname: String) -> String {
|
||||||
|
pick(
|
||||||
|
ko: "팔로우를 해제할까요?\n\(nickname)의 소식을 받을 수 없어요.",
|
||||||
|
en: "You won't receive updates from \(nickname).\nUnfollow this creator?",
|
||||||
|
ja: "\(nickname)からの最新情報を受け取れなくなります。\nフォローを解除しますか?"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
static var unfollowActionModalCancel: String { pick(ko: "취소", en: "Cancel", ja: "キャンセル") }
|
||||||
|
static var unfollowActionModalConfirm: String { pick(ko: "해제하기", en: "Unfollow", ja: "解除する") }
|
||||||
|
|
||||||
static var liveOnNow: String { pick(ko: "현재 라이브 중입니다.", en: "Live is currently ongoing.", ja: "現在ライブ配信中です。") }
|
static var liveOnNow: String { pick(ko: "현재 라이브 중입니다.", en: "Live is currently ongoing.", ja: "現在ライブ配信中です。") }
|
||||||
static var cannotReserveOwnLive: String { pick(ko: "내가 만든 라이브는 예약할 수 없습니다.", en: "reserve a live you created is required.", ja: "自分が作ったライブは予約できません。") }
|
static var cannotReserveOwnLive: String { pick(ko: "내가 만든 라이브는 예약할 수 없습니다.", en: "reserve a live you created is required.", ja: "自分が作ったライブは予約できません。") }
|
||||||
@@ -2842,7 +2851,7 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
static var channelDonationEmpty: String { pick(ko: "채널 후원이 없습니다.", en: "No channel donations.", ja: "チャンネル支援はありません。") }
|
static var channelDonationEmpty: String { pick(ko: "채널 후원이 없습니다.", en: "No channel donations.", ja: "チャンネル支援はありません。") }
|
||||||
static var channelDonationAllTitle: String { pick(ko: "채널 후원 전체보기", en: "All Channel Donations", ja: "チャンネル支援一覧") }
|
static var channelDonationAllTitle: String { pick(ko: "채널 후원 전체보기", en: "All Channel Donations", ja: "チャンネル支援一覧") }
|
||||||
static var totalLabel: String { pick(ko: "전체", en: "Total", ja: "全体") }
|
static var totalLabel: String { pick(ko: "전체", en: "Total", ja: "すべて") }
|
||||||
static var countUnit: String { pick(ko: "개", en: "items", ja: "件") }
|
static var countUnit: String { pick(ko: "개", en: "items", ja: "件") }
|
||||||
static var channelDonationDefaultMessage: String { pick(ko: "을 후원했습니다.", en: " donated.", ja: "を支援しました。") }
|
static var channelDonationDefaultMessage: String { pick(ko: "을 후원했습니다.", en: " donated.", ja: "を支援しました。") }
|
||||||
|
|
||||||
@@ -2860,7 +2869,7 @@ If you block this user, the following features will be restricted.
|
|||||||
static var creatorDetailDebut: String { pick(ko: "데뷔", en: "Debut", ja: "デビュー") }
|
static var creatorDetailDebut: String { pick(ko: "데뷔", en: "Debut", ja: "デビュー") }
|
||||||
static var creatorDetailTotalLiveCount: String { pick(ko: "라이브 총 횟수", en: "Total live sessions", ja: "ライブ総回数") }
|
static var creatorDetailTotalLiveCount: String { pick(ko: "라이브 총 횟수", en: "Total live sessions", ja: "ライブ総回数") }
|
||||||
static var creatorDetailAccumulatedLiveTime: String { pick(ko: "라이브 누적 시간", en: "Total live time", ja: "ライブ累積時間") }
|
static var creatorDetailAccumulatedLiveTime: String { pick(ko: "라이브 누적 시간", en: "Total live time", ja: "ライブ累積時間") }
|
||||||
static var creatorDetailAccumulatedParticipants: String { pick(ko: "라이브 누적 참여자", en: "Total live participants", ja: "ライブ累積参加者") }
|
static var creatorDetailAccumulatedParticipants: String { pick(ko: "라이브 누적 참여자", en: "Total live participants", ja: "ライブ累計参加者数") }
|
||||||
static var creatorDetailRegisteredContentCount: String { pick(ko: "등록 콘텐츠 수", en: "Registered contents", ja: "登録コンテンツ数") }
|
static var creatorDetailRegisteredContentCount: String { pick(ko: "등록 콘텐츠 수", en: "Registered contents", ja: "登録コンテンツ数") }
|
||||||
static var creatorDetailSns: String { pick(ko: "SNS", en: "SNS", ja: "SNS") }
|
static var creatorDetailSns: String { pick(ko: "SNS", en: "SNS", ja: "SNS") }
|
||||||
static var preDebut: String { pick(ko: "데뷔전", en: "Pre-debut", ja: "デビュー前") }
|
static var preDebut: String { pick(ko: "데뷔전", en: "Pre-debut", ja: "デビュー前") }
|
||||||
@@ -2870,7 +2879,7 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func shareChannelMessage(_ nickname: String) -> String {
|
static func shareChannelMessage(_ nickname: String) -> String {
|
||||||
pick(ko: "보이스온 \(nickname)님의 채널입니다.", en: "This is \(nickname)'s channel on VoiceOn.", ja: "ボイスオンの\(nickname)さんのチャンネルです。")
|
pick(ko: "소다라이브 \(nickname)님의 채널입니다.", en: "This is \(nickname)'s channel on SodaLive.", ja: "ソーダライブの\(nickname)さんのチャンネルです。")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func reserveWithCansTitle(_ price: Int) -> String {
|
static func reserveWithCansTitle(_ price: Int) -> String {
|
||||||
@@ -2966,11 +2975,7 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var totalPrefix: String {
|
static var totalPrefix: String {
|
||||||
pick(ko: "총", en: "Total", ja: "合計")
|
pick(ko: "전체", en: "Total", ja: "すべて")
|
||||||
}
|
|
||||||
|
|
||||||
static var personUnit: String {
|
|
||||||
pick(ko: "명", en: "people", ja: "人")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static var emptyFollowingChannels: String {
|
static var emptyFollowingChannels: String {
|
||||||
@@ -3036,7 +3041,7 @@ If you block this user, the following features will be restricted.
|
|||||||
static var content: String { pick(ko: "콘텐츠", en: "Content", ja: "コンテンツ") }
|
static var content: String { pick(ko: "콘텐츠", en: "Content", ja: "コンテンツ") }
|
||||||
static var live: String { pick(ko: "라이브", en: "Live", ja: "ライブ") }
|
static var live: String { pick(ko: "라이브", en: "Live", ja: "ライブ") }
|
||||||
static var chat: String { pick(ko: "대화", en: "Talks", ja: "トーク") }
|
static var chat: String { pick(ko: "대화", en: "Talks", ja: "トーク") }
|
||||||
static var my: String { pick(ko: "마이", en: "My", ja: "マイ") }
|
static var my: String { pick(ko: "마이", en: "My", ja: "マイページ") }
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Auth {
|
enum Auth {
|
||||||
@@ -3095,8 +3100,8 @@ If you block this user, the following features will be restricted.
|
|||||||
pick(ko: "New&Hot", en: "New&Hot", ja: "New&Hot")
|
pick(ko: "New&Hot", en: "New&Hot", ja: "New&Hot")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var voiceOnOnlySectionTitle: String {
|
static var sodaLiveOnlySectionTitle: String {
|
||||||
pick(ko: "오직 보이스온에서만!", en: "Only on VoiceOn!", ja: "VoiceOnだけで!")
|
pick(ko: "오직 소다라이브에서만!", en: "Only on SodaLive!", ja: "SODALIVE限定")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var freeAudioSectionTitle: String {
|
static var freeAudioSectionTitle: String {
|
||||||
@@ -3127,7 +3132,7 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var risingTabTitle: String {
|
static var risingTabTitle: String {
|
||||||
pick(ko: "지금 뜨는 중", en: "Rising now", ja: "今話題")
|
pick(ko: "지금 뜨는 중", en: "Rising now", ja: "急上昇")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var revenueTabTitle: String {
|
static var revenueTabTitle: String {
|
||||||
@@ -3217,8 +3222,8 @@ If you block this user, the following features will be restricted.
|
|||||||
pick(ko: "인기 크리에이터", en: "Popular creators", ja: "人気クリエイター")
|
pick(ko: "인기 크리에이터", en: "Popular creators", ja: "人気クリエイター")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var onlyOnVoiceOnSectionTitle: String {
|
static var onlyOnSodaLiveSectionTitle: String {
|
||||||
pick(ko: "오직 보이스온에서만", en: "Only on VoiceOn", ja: "VoiceOnだけで")
|
pick(ko: "오직 소다라이브에서만", en: "Only on SodaLive", ja: "SodaLiveだけで")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var popularCharacterChatSectionTitle: String {
|
static var popularCharacterChatSectionTitle: String {
|
||||||
@@ -3242,7 +3247,7 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var weeklyChartSectionTitle: String {
|
static var weeklyChartSectionTitle: String {
|
||||||
pick(ko: "보온 주간 차트", en: "VoiceOn weekly chart", ja: "ボイスオン週間チャート")
|
pick(ko: "보온 주간 차트", en: "SodaLive weekly chart", ja: "ソーダライブ週間チャート")
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RecommendChannel {
|
enum RecommendChannel {
|
||||||
@@ -3255,9 +3260,9 @@ If you block this user, the following features will be restricted.
|
|||||||
enum HomeFollowing {
|
enum HomeFollowing {
|
||||||
static var emptyStateMessage: String {
|
static var emptyStateMessage: String {
|
||||||
pick(
|
pick(
|
||||||
ko: "아직 팔로잉 소식이 없어요.\n관심 있는 크리에이터를 팔로우해 보세요.",
|
ko: "아직 팔로우한 크리에이터가 없어요.\n관심 있는 크리에이터를 팔로우해 보세요.",
|
||||||
en: "There are no following updates yet.\nFollow creators you're interested in.",
|
en: "There are no following updates yet.\nFollow creators you're interested in.",
|
||||||
ja: "まだフォロー中の新着情報がありません。\n気になるクリエイターをフォローしてみましょう。"
|
ja: "まだフォローしているクリエイターがいません。\n気になるクリエイターをフォローしてみましょう。"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3334,14 +3339,14 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var recentNewsTitle: String {
|
static var recentNewsTitle: String {
|
||||||
pick(ko: "최근 소식", en: "Recent news", ja: "最近のニュース")
|
pick(ko: "최근 소식", en: "Recent news", ja: "最新情報")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func rankingMessagePrefix(_ title: String) -> String {
|
static func rankingMessagePrefix(_ title: String) -> String {
|
||||||
pick(
|
pick(
|
||||||
ko: "\(title)님이 이번 주 랭킹 ",
|
ko: "\(title)님이 이번 주 랭킹 ",
|
||||||
en: "\(title) reached ",
|
en: "\(title) reached ",
|
||||||
ja: "\(title)さんが今週のランキング"
|
ja: "\(title)が今週のランキングで"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3350,7 +3355,7 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var rankingMessageSuffix: String {
|
static var rankingMessageSuffix: String {
|
||||||
pick(ko: "에 올랐어요!", en: " in this week's ranking!", ja: "に入りました!")
|
pick(ko: "에 올랐어요!", en: " in this week's ranking!", ja: "にランクイン!")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var audioTag: String {
|
static var audioTag: String {
|
||||||
@@ -3373,7 +3378,7 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum CreatorChannelLive {
|
enum CreatorChannelLive {
|
||||||
static var totalLabel: String { pick(ko: "전체", en: "Total", ja: "全体") }
|
static var totalLabel: String { pick(ko: "전체", en: "Total", ja: "すべて") }
|
||||||
static var startLive: String { pick(ko: "라이브 시작하기", en: "Start live", ja: "ライブを開始") }
|
static var startLive: String { pick(ko: "라이브 시작하기", en: "Start live", ja: "ライブを開始") }
|
||||||
static var emptyMessage: String {
|
static var emptyMessage: String {
|
||||||
pick(
|
pick(
|
||||||
@@ -3386,25 +3391,25 @@ If you block this user, the following features will be restricted.
|
|||||||
enum Sort {
|
enum Sort {
|
||||||
static var latest: String { pick(ko: "최신순", en: "Latest", ja: "最新順") }
|
static var latest: String { pick(ko: "최신순", en: "Latest", ja: "最新順") }
|
||||||
static var popular: String { pick(ko: "인기순", en: "Popular", ja: "人気順") }
|
static var popular: String { pick(ko: "인기순", en: "Popular", ja: "人気順") }
|
||||||
static var priceHigh: String { pick(ko: "높은 가격순", en: "Highest price", ja: "価格が高い順") }
|
static var priceHigh: String { pick(ko: "높은 가격순", en: "Highest price", ja: "高い順") }
|
||||||
static var priceLow: String { pick(ko: "낮은 가격순", en: "Lowest price", ja: "価格が低い順") }
|
static var priceLow: String { pick(ko: "낮은 가격순", en: "Lowest price", ja: "安い順") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CreatorChannelAudio {
|
enum CreatorChannelAudio {
|
||||||
static var totalLabel: String { pick(ko: "전체", en: "Total", ja: "全体") }
|
static var totalLabel: String { pick(ko: "전체", en: "Total", ja: "すべて") }
|
||||||
static var countUnit: String { pick(ko: "개", en: "", ja: "件") }
|
static var countUnit: String { pick(ko: "개", en: "", ja: "件") }
|
||||||
static var ownershipPrefix: String {
|
static var ownershipPrefix: String {
|
||||||
pick(ko: "전체 오디오의 ", en: "You own ", ja: "全オーディオの")
|
pick(ko: "전체 오디오의 ", en: "You own ", ja: "全オーディオの")
|
||||||
}
|
}
|
||||||
static var ownershipSuffix: String {
|
static var ownershipSuffix: String {
|
||||||
pick(ko: "를 소장하고 있어요.", en: " of all audio.", ja: "を保有中です。")
|
pick(ko: "를 소장하고 있어요.", en: " of all audio.", ja: "を所持しています。")
|
||||||
}
|
}
|
||||||
static var emptyMessage: String {
|
static var emptyMessage: String {
|
||||||
pick(
|
pick(
|
||||||
ko: "크리에이터가 오디오를 준비 중입니다.\n기대해 주세요!",
|
ko: "크리에이터가 오디오를 준비 중입니다.\n기대해 주세요!",
|
||||||
en: "The creator is preparing audio.\nPlease stay tuned!",
|
en: "The creator is preparing audio.\nPlease stay tuned!",
|
||||||
ja: "クリエイターがオーディオを準備中です。\nお楽しみに!"
|
ja: "クリエイターがオーディオを準備しています。\nお楽しみに!"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3417,42 +3422,58 @@ If you block this user, the following features will be restricted.
|
|||||||
pick(
|
pick(
|
||||||
ko: "크리에이터가 시리즈를 준비 중입니다.\n기대해 주세요!",
|
ko: "크리에이터가 시리즈를 준비 중입니다.\n기대해 주세요!",
|
||||||
en: "The creator is preparing series.\nPlease stay tuned!",
|
en: "The creator is preparing series.\nPlease stay tuned!",
|
||||||
ja: "クリエイターがシリーズを準備中です。\nお楽しみに!"
|
ja: "クリエイターがシリーズを準備しています。\nお楽しみに!"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func totalContentCount(_ count: Int) -> String {
|
static func totalContentCount(_ count: Int) -> String {
|
||||||
pick(ko: "총 \(count)화", en: "Total \(count) episodes", ja: "全\(count)話")
|
pick(ko: "전체 \(count)화", en: "Total \(count) episodes", ja: "すべて\(count)話")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CreatorChannelHome {
|
enum CreatorChannelHome {
|
||||||
|
static var home: String { pick(ko: "홈", en: "Home", ja: "ホーム") }
|
||||||
|
static var live: String { pick(ko: "라이브", en: "Live", ja: "ライブ") }
|
||||||
static var currentLive: String { pick(ko: "현재 라이브", en: "Live now", ja: "現在ライブ") }
|
static var currentLive: String { pick(ko: "현재 라이브", en: "Live now", ja: "現在ライブ") }
|
||||||
static var latestAudio: String { pick(ko: "최신 오디오 콘텐츠", en: "Latest audio", ja: "最新オーディオ") }
|
static var latestAudio: String { pick(ko: "최신 오디오 콘텐츠", en: "Latest audio", ja: "最新オーディオ") }
|
||||||
static var donation: String { pick(ko: "후원", en: "Donations", ja: "応援") }
|
static var donation: String { pick(ko: "후원", en: "Donations", ja: "サポート") }
|
||||||
|
static var donationEmptyMessage: String {
|
||||||
|
pick(
|
||||||
|
ko: "처음으로 크리에이터를\n후원해 보세요!",
|
||||||
|
en: "Be the first to\nsupport this creator!",
|
||||||
|
ja: "最初のサポートをしてみましょう!"
|
||||||
|
)
|
||||||
|
}
|
||||||
static var notice: String { pick(ko: "공지", en: "Notice", ja: "お知らせ") }
|
static var notice: String { pick(ko: "공지", en: "Notice", ja: "お知らせ") }
|
||||||
static var schedule: String { pick(ko: "스케줄", en: "Schedule", ja: "スケジュール") }
|
static var schedule: String { pick(ko: "스케줄", en: "Schedule", ja: "スケジュール") }
|
||||||
static var audio: String { pick(ko: "오디오", en: "Audio", ja: "オーディオ") }
|
static var audio: String { pick(ko: "오디오", en: "Audio", ja: "オーディオ") }
|
||||||
static var series: String { pick(ko: "시리즈", en: "Series", ja: "シリーズ") }
|
static var series: String { pick(ko: "시리즈", en: "Series", ja: "シリーズ") }
|
||||||
static var community: String { pick(ko: "커뮤니티", en: "Community", ja: "コミュニティ") }
|
static var community: String { pick(ko: "커뮤니티", en: "Community", ja: "コミュニティ") }
|
||||||
static var fanTalk: String { pick(ko: "팬Talk", en: "Fan Talk", ja: "ファンTalk") }
|
static var fanTalk: String { pick(ko: "팬레터", en: "Fan Letter", ja: "ファンレター") }
|
||||||
|
static var fanTalkEmptyMessage: String {
|
||||||
|
pick(
|
||||||
|
ko: "당신의 첫 응원을\n기다리고 있어요!",
|
||||||
|
en: "We're waiting for\nyour first fan letter!",
|
||||||
|
ja: "あなたの最初のファンレターを待っています!"
|
||||||
|
)
|
||||||
|
}
|
||||||
static var introduce: String { pick(ko: "소개", en: "Introduction", ja: "紹介") }
|
static var introduce: String { pick(ko: "소개", en: "Introduction", ja: "紹介") }
|
||||||
static var activity: String { pick(ko: "활동", en: "Activity", ja: "活動") }
|
static var activity: String { pick(ko: "활동", en: "Activity", ja: "アクティビティ") }
|
||||||
static var sns: String { pick(ko: "SNS", en: "SNS", ja: "SNS") }
|
static var sns: String { pick(ko: "SNS", en: "SNS", ja: "SNS") }
|
||||||
static var follow: String { pick(ko: "팔로우", en: "Follow", ja: "フォロー") }
|
static var follow: String { pick(ko: "팔로우", en: "Follow", ja: "フォロー") }
|
||||||
static var following: String { pick(ko: "팔로잉", en: "Following", ja: "フォロー中") }
|
static var following: String { pick(ko: "팔로잉", en: "Following", ja: "フォロー中") }
|
||||||
static var talk: String { pick(ko: "대화하기", en: "Start chat", ja: "会話する") }
|
static var talk: String { pick(ko: "대화하기", en: "Start chat", ja: "トークする") }
|
||||||
static var dm: String { pick(ko: "DM 보내기", en: "Send DM", ja: "DMを送る") }
|
static var dm: String { pick(ko: "DM 보내기", en: "Send DM", ja: "DMを送る") }
|
||||||
static var checkDm: String { pick(ko: "DM 확인하기", en: "Check DM", ja: "DMを確認") }
|
static var checkDm: String { pick(ko: "DM 확인하기", en: "Check DM", ja: "DMを確認") }
|
||||||
static var uploadCommunityPost: String { pick(ko: "커뮤니티 글 올리기", en: "Upload community post", ja: "コミュニティ投稿を作成") }
|
static var uploadCommunityPost: String { pick(ko: "커뮤니티 글 올리기", en: "Upload community post", ja: "コミュニティ投稿") }
|
||||||
static var uploadAudioContent: String { pick(ko: "오디오 콘텐츠 올리기", en: "Upload audio content", ja: "オーディオコンテンツを投稿") }
|
static var uploadAudioContent: String { pick(ko: "오디오 콘텐츠 올리기", en: "Upload audio content", ja: "オーディオコンテンツを投稿") }
|
||||||
static var createLive: String { pick(ko: "라이브 만들기", en: "Create live", ja: "ライブを作成") }
|
static var createLive: String { pick(ko: "라이브 만들기", en: "Create live", ja: "ライブを作成") }
|
||||||
static var closeFloatingMenu: String { pick(ko: "닫기", en: "Close", ja: "閉じる") }
|
static var closeFloatingMenu: String { pick(ko: "닫기", en: "Close", ja: "閉じる") }
|
||||||
static var viewAll: String { pick(ko: "전체보기", en: "View all", ja: "すべて見る") }
|
static var viewAll: String { pick(ko: "전체보기", en: "View all", ja: "すべて見る") }
|
||||||
static var debut: String { pick(ko: "데뷔", en: "Debut", ja: "デビュー") }
|
static var debut: String { pick(ko: "데뷔", en: "Debut", ja: "デビュー") }
|
||||||
static var liveCount: String { pick(ko: "라이브 총 진행 수", en: "Total live count", ja: "ライブ総配信数") }
|
static var liveCount: String { pick(ko: "라이브 총 진행 수", en: "Total live count", ja: "ライブ配信回数") }
|
||||||
static var liveDurationHours: String { pick(ko: "라이브 누적 진행 시간", en: "Total live hours", ja: "ライブ累計配信時間") }
|
static var liveDurationHours: String { pick(ko: "라이브 누적 진행 시간", en: "Total live hours", ja: "ライブ累計配信時間") }
|
||||||
static var liveContributorCount: String { pick(ko: "라이브 누적 참여자", en: "Total live participants", ja: "ライブ累計参加者") }
|
static var liveContributorCount: String { pick(ko: "라이브 누적 참여자", en: "Total live participants", ja: "ライブ累計参加者数") }
|
||||||
static var audioContentCount: String { pick(ko: "오디오", en: "Audio", ja: "オーディオ") }
|
static var audioContentCount: String { pick(ko: "오디오", en: "Audio", ja: "オーディオ") }
|
||||||
static var seriesCount: String { pick(ko: "시리즈", en: "Series", ja: "シリーズ") }
|
static var seriesCount: String { pick(ko: "시리즈", en: "Series", ja: "シリーズ") }
|
||||||
static var placeholderMessage: String {
|
static var placeholderMessage: String {
|
||||||
@@ -3489,15 +3510,15 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum CreatorChannelCommunity {
|
enum CreatorChannelCommunity {
|
||||||
static var totalLabel: String { pick(ko: "전체", en: "Total", ja: "全体") }
|
static var totalLabel: String { pick(ko: "전체", en: "Total", ja: "すべて") }
|
||||||
static var listView: String { pick(ko: "리스트형", en: "List", ja: "リスト型") }
|
static var listView: String { pick(ko: "리스트형", en: "List", ja: "リスト") }
|
||||||
static var thumbnailView: String { pick(ko: "썸네일형", en: "Thumbnail", ja: "サムネイル型") }
|
static var thumbnailView: String { pick(ko: "썸네일형", en: "Thumbnail", ja: "グリッド") }
|
||||||
static var commentPlaceholder: String { pick(ko: "댓글을 입력하세요", en: "Enter a comment", ja: "コメントを入力してください") }
|
static var commentPlaceholder: String { pick(ko: "댓글을 입력하세요", en: "Enter a comment", ja: "コメントを入力") }
|
||||||
static var communityEmpty: String {
|
static var communityEmpty: String {
|
||||||
pick(
|
pick(
|
||||||
ko: "크리에이터가 커뮤니티를 준비 중입니다.\n기대해 주세요!",
|
ko: "크리에이터가 커뮤니티를 준비 중입니다.\n기대해 주세요!",
|
||||||
en: "The creator is preparing the community.\nPlease look forward to it!",
|
en: "The creator is preparing the community.\nPlease look forward to it!",
|
||||||
ja: "クリエイターがコミュニティを準備中です。\nお楽しみに!"
|
ja: "クリエイターがコミュニティを準備しています。\nお楽しみに!"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3505,7 +3526,7 @@ If you block this user, the following features will be restricted.
|
|||||||
|
|
||||||
enum CreatorChannelReplyDetail {
|
enum CreatorChannelReplyDetail {
|
||||||
static var replyPlaceholder: String {
|
static var replyPlaceholder: String {
|
||||||
pick(ko: "답글을 입력하세요", en: "Enter a reply", ja: "返信を入力してください")
|
pick(ko: "답글을 입력하세요", en: "Enter a reply", ja: "返信を入力")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var replaceReplyTitle: String {
|
static var replaceReplyTitle: String {
|
||||||
@@ -3517,16 +3538,43 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var replaceReplyConfirm: String {
|
static var replaceReplyConfirm: String {
|
||||||
pick(ko: "수정하기", en: "Update", ja: "修正する")
|
pick(ko: "수정하기", en: "Update", ja: "編集")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CreatorChannelFanTalk {
|
enum CreatorChannelFanTalk {
|
||||||
|
static var emptyMessage: String {
|
||||||
|
pick(
|
||||||
|
ko: "아직 응원이 없습니다.\n처음으로 크리에이터를 응원해 보세요!",
|
||||||
|
en: "No fan letters yet.\nBe the first to send one!",
|
||||||
|
ja: "まだファンレターがありません。\n最初のファンレターを送ってみましょう!"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
static var writeAction: String { pick(ko: "응원남기기", en: "Send a fan letter", ja: "ファンレターを送る") }
|
||||||
|
static var writePlaceholder: String { pick(ko: "응원 메시지를 입력해 주세요.", en: "Enter a message of support.", ja: "応援メッセージを入力してください。") }
|
||||||
static var send: String { pick(ko: "보내기", en: "Send", ja: "送信") }
|
static var send: String { pick(ko: "보내기", en: "Send", ja: "送信") }
|
||||||
static var exitModalTitle: String { pick(ko: "입력 종료", en: "Discard input", ja: "入力終了") }
|
static var exitModalTitle: String { pick(ko: "입력 종료", en: "Discard input", ja: "入力終了") }
|
||||||
static var exitModalMessage: String { pick(ko: "입력을 종료할까요?\n지금 나가면 입력한 내용이 저장되지 않아요.", en: "Discard your input?\nIf you leave now, your input will not be saved.", ja: "入力を終了しますか?\n今退出すると入力した内容は保存されません。") }
|
static var exitModalMessage: String { pick(ko: "입력을 종료할까요?\n지금 나가면 입력한 내용이 저장되지 않아요.", en: "Discard your input?\nIf you leave now, your input will not be saved.", ja: "入力を終了しますか?\n終了すると、入力内容は保存されません。") }
|
||||||
static var exitModalExit: String { pick(ko: "종료", en: "Discard", ja: "終了") }
|
static var exitModalExit: String { pick(ko: "종료", en: "Discard", ja: "終了") }
|
||||||
static var exitModalContinue: String { pick(ko: "계속 작성", en: "Keep writing", ja: "続けて書く") }
|
static var exitModalContinue: String { pick(ko: "계속 작성", en: "Keep writing", ja: "続ける") }
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CreatorChannelDonation {
|
||||||
|
static var emptyMessage: String {
|
||||||
|
pick(
|
||||||
|
ko: "아직 후원이 없습니다.\n처음으로 크리에이터를 후원해 보세요!",
|
||||||
|
en: "No support yet.\nBe the first to support this creator!",
|
||||||
|
ja: "まだサポートがありません。\n最初のサポートをしてみましょう!"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func defaultMessage(_ can: Int) -> String {
|
||||||
|
pick(
|
||||||
|
ko: "\(can)캔을 후원하였습니다",
|
||||||
|
en: "Donated \(can) cans.",
|
||||||
|
ja: "\(can)canをサポートしました。"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum HomeRecommendation {
|
enum HomeRecommendation {
|
||||||
@@ -3555,7 +3603,7 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var recentDebutCreatorSectionTitle: String {
|
static var recentDebutCreatorSectionTitle: String {
|
||||||
pick(ko: "최근 데뷔한 크리에이터", en: "Recently debuted creators", ja: "最近デビューしたクリエイター")
|
pick(ko: "최근 데뷔한 크리에이터", en: "Recently debuted creators", ja: "新着クリエイター")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var firstAudioContentSectionTitle: String {
|
static var firstAudioContentSectionTitle: String {
|
||||||
@@ -3563,7 +3611,7 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var aiCharacterSectionTitle: String {
|
static var aiCharacterSectionTitle: String {
|
||||||
pick(ko: "크리에이터와 이야기를 나눠요!", en: "AI characters", ja: "AIキャラクター")
|
pick(ko: "크리에이터와 이야기를 나눠요!", en: "AI characters", ja: "クリエイターと話す")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func compactChatCount(_ count: Int) -> String {
|
static func compactChatCount(_ count: Int) -> String {
|
||||||
@@ -3622,7 +3670,7 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var cheerCreatorSectionTitle: String {
|
static var cheerCreatorSectionTitle: String {
|
||||||
pick(ko: "최근 응원이 많은 크리에이터", en: "Recently cheered creators", ja: "最近応援が多いクリエイター")
|
pick(ko: "최근 응원이 많은 크리에이터", en: "Recently cheered creators", ja: "最近応援の多いクリエイター")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var popularCommunitySectionTitle: String {
|
static var popularCommunitySectionTitle: String {
|
||||||
@@ -3638,11 +3686,11 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var followAllConfirmTitle: String {
|
static var followAllConfirmTitle: String {
|
||||||
pick(ko: "모두 팔로우할까요?", en: "Follow all creators?", ja: "すべてのクリエイターをフォローしますか?")
|
pick(ko: "모두 팔로우 하기", en: "Follow all", ja: "すべてフォロー")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var followAllConfirmDescription: String {
|
static var followAllConfirmDescription: String {
|
||||||
pick(ko: "추천된 크리에이터를 모두 팔로우합니다.", en: "You’ll follow all recommended creators.", ja: "おすすめのクリエイターをすべてフォローします。")
|
pick(ko: "추천 크리에이터를 모두 팔로우하고,\n소식을 받아보세요!", en: "Follow all recommended creators\nand get their updates!", ja: "おすすめのクリエイターをすべてフォローして、\n最新情報を受け取りましょう!")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var followAllConfirmAction: String {
|
static var followAllConfirmAction: String {
|
||||||
@@ -3708,11 +3756,11 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var editAction: String {
|
static var editAction: String {
|
||||||
pick(ko: "수정하기", en: "Edit", ja: "編集する")
|
pick(ko: "수정하기", en: "Edit", ja: "編集")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var deleteAction: String {
|
static var deleteAction: String {
|
||||||
pick(ko: "삭제하기", en: "Delete", ja: "削除する")
|
pick(ko: "삭제하기", en: "Delete", ja: "削除")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var report: String {
|
static var report: String {
|
||||||
@@ -3760,7 +3808,7 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var pinRelease: String {
|
static var pinRelease: String {
|
||||||
pick(ko: "고정 해제", en: "Unpin", ja: "固定解除")
|
pick(ko: "고정 해제", en: "Unpin", ja: "固定を解除")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var pinToTop: String {
|
static var pinToTop: String {
|
||||||
@@ -3864,11 +3912,11 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var fanTalkTitle: String {
|
static var fanTalkTitle: String {
|
||||||
pick(ko: "팬 Talk", en: "Fan Talk", ja: "ファントーク")
|
pick(ko: "팬레터", en: "Fan Letter", ja: "ファンレター")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var fanTalkAllTitle: String {
|
static var fanTalkAllTitle: String {
|
||||||
pick(ko: "팬 Talk 전체보기", en: "All Fan Talk", ja: "ファントーク一覧")
|
pick(ko: "팬레터 전체보기", en: "All Fan Letters", ja: "ファンレター一覧")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var cheerTitle: String {
|
static var cheerTitle: String {
|
||||||
@@ -3992,9 +4040,9 @@ If you block this user, the following features will be restricted.
|
|||||||
enum ApplyAuditionComplete {
|
enum ApplyAuditionComplete {
|
||||||
static var thankYouDescription: String {
|
static var thankYouDescription: String {
|
||||||
pick(
|
pick(
|
||||||
ko: "보이스온 오디션에 지원해 주셔서 감사합니다.",
|
ko: "소다라이브 오디션에 지원해 주셔서 감사합니다.",
|
||||||
en: "Thank you for applying to the VoiceOn audition.",
|
en: "Thank you for applying to the SodaLive audition.",
|
||||||
ja: "VoiceOnオーディションにご応募いただきありがとうございます。"
|
ja: "SodaLiveオーディションにご応募いただきありがとうございます。"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4117,7 +4165,7 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var archive: String {
|
static var archive: String {
|
||||||
pick(ko: "보관함", en: "Archived", ja: "保管済み")
|
pick(ko: "보관함", en: "Archived", ja: "ライブラリ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4304,7 +4352,7 @@ If you block this user, the following features will be restricted.
|
|||||||
static var completedSectionTitle: String { pick(ko: "완결 시리즈", en: "Completed series", ja: "完結シリーズ") }
|
static var completedSectionTitle: String { pick(ko: "완결 시리즈", en: "Completed series", ja: "完結シリーズ") }
|
||||||
static var recommendedSectionTitle: String { pick(ko: "추천 시리즈", en: "Recommended series", ja: "おすすめシリーズ") }
|
static var recommendedSectionTitle: String { pick(ko: "추천 시리즈", en: "Recommended series", ja: "おすすめシリーズ") }
|
||||||
static var byDaySectionTitle: String { pick(ko: "요일별 시리즈", en: "Series by day", ja: "曜日別シリーズ") }
|
static var byDaySectionTitle: String { pick(ko: "요일별 시리즈", en: "Series by day", ja: "曜日別シリーズ") }
|
||||||
static var voiceOnOnlyTitle: String { pick(ko: "오직 보이스온에서만", en: "Only on VoiceOn", ja: "VoiceOn限定") }
|
static var sodaLiveOnlyTitle: String { pick(ko: "오직 소다라이브에서만", en: "Only on SodaLive", ja: "SodaLive限定") }
|
||||||
static var allEpisodesListen: String { pick(ko: "전체회차 듣기", en: "Listen to all episodes", ja: "全話を聴く") }
|
static var allEpisodesListen: String { pick(ko: "전체회차 듣기", en: "Listen to all episodes", ja: "全話を聴く") }
|
||||||
static func allEpisodesTitle(_ seriesTitle: String) -> String { pick(ko: "\(seriesTitle) - 전체회차 듣기", en: "\(seriesTitle) - All episodes", ja: "\(seriesTitle) - 全話を聴く") }
|
static func allEpisodesTitle(_ seriesTitle: String) -> String { pick(ko: "\(seriesTitle) - 전체회차 듣기", en: "\(seriesTitle) - All episodes", ja: "\(seriesTitle) - 全話を聴く") }
|
||||||
static var registeredOrder: String { pick(ko: "등록순", en: "Oldest", ja: "登録順") }
|
static var registeredOrder: String { pick(ko: "등록순", en: "Oldest", ja: "登録順") }
|
||||||
@@ -4321,7 +4369,7 @@ If you block this user, the following features will be restricted.
|
|||||||
static var age19Badge: String { pick(ko: "19세", en: "19+", ja: "19+") }
|
static var age19Badge: String { pick(ko: "19세", en: "19+", ja: "19+") }
|
||||||
static func publishing(_ days: String) -> String { pick(ko: "\(days) 연재", en: "\(days)", ja: "\(days)連載") }
|
static func publishing(_ days: String) -> String { pick(ko: "\(days) 연재", en: "\(days)", ja: "\(days)連載") }
|
||||||
static var totalEpisodes: (Int) -> String = { count in
|
static var totalEpisodes: (Int) -> String = { count in
|
||||||
pick(ko: "총 \(count)화", en: "Total \(count) episodes", ja: "全\(count)話")
|
pick(ko: "전체 \(count)화", en: "Total \(count) episodes", ja: "すべて\(count)話")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 시리즈 전체보기(크리에이터 기준) 타이틀
|
// 시리즈 전체보기(크리에이터 기준) 타이틀
|
||||||
@@ -4526,11 +4574,7 @@ If you block this user, the following features will be restricted.
|
|||||||
enum MyPage {
|
enum MyPage {
|
||||||
enum Common {
|
enum Common {
|
||||||
static var totalPrefix: String {
|
static var totalPrefix: String {
|
||||||
pick(ko: "총", en: "Total", ja: "合計")
|
pick(ko: "전체", en: "Total", ja: "すべて")
|
||||||
}
|
|
||||||
|
|
||||||
static var personUnit: String {
|
|
||||||
pick(ko: "명", en: "people", ja: "人")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static var countUnit: String {
|
static var countUnit: String {
|
||||||
@@ -4584,7 +4628,7 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var chargeAction: String {
|
static var chargeAction: String {
|
||||||
pick(ko: "충전하기", en: "Charge", ja: "チャージする")
|
pick(ko: "충전하기", en: "Charge", ja: "チャージ")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var chargeShort: String {
|
static var chargeShort: String {
|
||||||
@@ -4686,9 +4730,9 @@ If you block this user, the following features will be restricted.
|
|||||||
|
|
||||||
static var noticeTermsReference: String {
|
static var noticeTermsReference: String {
|
||||||
pick(
|
pick(
|
||||||
ko: "자세한 내용은 보이스온 이용약관에서 확인할 수 있습니다.",
|
ko: "자세한 내용은 소다라이브 이용약관에서 확인할 수 있습니다.",
|
||||||
en: "For details, please check the VoiceOn Terms of Service.",
|
en: "For details, please check the SodaLive Terms of Service.",
|
||||||
ja: "詳細はVoiceOn利用規約をご確認ください。"
|
ja: "詳細はSodaLive利用規約をご確認ください。"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4782,7 +4826,7 @@ If you block this user, the following features will be restricted.
|
|||||||
|
|
||||||
enum Category {
|
enum Category {
|
||||||
static var storage: String {
|
static var storage: String {
|
||||||
pick(ko: "보관함", en: "Library", ja: "保管庫")
|
pick(ko: "보관함", en: "Library", ja: "ライブラリ")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var blockList: String {
|
static var blockList: String {
|
||||||
@@ -4816,7 +4860,7 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var owned: String {
|
static var owned: String {
|
||||||
pick(ko: "소장중", en: "Owned", ja: "所持中")
|
pick(ko: "소장중", en: "Owned", ja: "保有中")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5056,7 +5100,7 @@ If you block this user, the following features will be restricted.
|
|||||||
|
|
||||||
enum ServiceCenter {
|
enum ServiceCenter {
|
||||||
static var buttonTitle: String {
|
static var buttonTitle: String {
|
||||||
pick(ko: "보이스온 고객센터", en: "VoiceOn customer center", ja: "VoiceOnカスタマーセンター")
|
pick(ko: "소다라이브 고객센터", en: "SodaLive customer center", ja: "SodaLiveカスタマーセンター")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var talkInquiry: String {
|
static var talkInquiry: String {
|
||||||
@@ -5079,11 +5123,11 @@ If you block this user, the following features will be restricted.
|
|||||||
|
|
||||||
enum User {
|
enum User {
|
||||||
static var emailTitle: String {
|
static var emailTitle: String {
|
||||||
pick(ko: "이메일", en: "Email", ja: "メール")
|
pick(ko: "이메일", en: "Email", ja: "メールアドレス")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var emailPlaceholder: String {
|
static var emailPlaceholder: String {
|
||||||
pick(ko: "이메일", en: "Email", ja: "メール")
|
pick(ko: "이메일", en: "Email", ja: "メールアドレス")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var passwordTitle: String {
|
static var passwordTitle: String {
|
||||||
@@ -5137,7 +5181,7 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var privacyPolicy: String {
|
static var privacyPolicy: String {
|
||||||
pick(ko: "개인정보수집 및 이용동의", en: "Consent to collect and use personal information", ja: "個人情報の収集および利用への同意")
|
pick(ko: "개인정보수집 및 이용동의", en: "Consent to collect and use personal information", ja: "個人情報の収集・利用への同意")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var required: String {
|
static var required: String {
|
||||||
@@ -5201,7 +5245,7 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var signUpPrompt: String {
|
static var signUpPrompt: String {
|
||||||
pick(ko: "보이스온 회원이 아닌가요? 지금 가입하세요.", en: "Not a VoiceOn member? Sign up now.", ja: "VoiceOnの会員ではありませんか?今すぐ登録してください。")
|
pick(ko: "소다라이브 회원이 아닌가요? 지금 가입하세요.", en: "Not a SodaLive member? Sign up now.", ja: "SodaLiveの会員ではありませんか?今すぐ登録してください。")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var appleAuthorizationFailed: String {
|
static var appleAuthorizationFailed: String {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ struct LiveRoomDonationRankingDialog: View {
|
|||||||
LiveRoomDonationRankingTotalCanView(totalCan: donationStatus.totalCan, totalSecretCan: donationStatus.totalSecretCan)
|
LiveRoomDonationRankingTotalCanView(totalCan: donationStatus.totalCan, totalSecretCan: donationStatus.totalSecretCan)
|
||||||
.padding(.top, 25)
|
.padding(.top, 25)
|
||||||
|
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 4) {
|
||||||
Text(I18n.CreateLive.totalLabel)
|
Text(I18n.CreateLive.totalLabel)
|
||||||
.appFont(size: 14.7, weight: .medium)
|
.appFont(size: 14.7, weight: .medium)
|
||||||
.foregroundColor(Color.grayee)
|
.foregroundColor(Color.grayee)
|
||||||
@@ -42,10 +42,6 @@ struct LiveRoomDonationRankingDialog: View {
|
|||||||
.foregroundColor(Color.button)
|
.foregroundColor(Color.button)
|
||||||
.padding(.leading, 6.7)
|
.padding(.leading, 6.7)
|
||||||
|
|
||||||
Text(I18n.CreateLive.peopleUnit)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.foregroundColor(Color.gray77)
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.top, 13.3)
|
.padding(.top, 13.3)
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ struct LiveRoomHeartRankingDialog: View {
|
|||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
.padding(.top, 25)
|
.padding(.top, 25)
|
||||||
|
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 4) {
|
||||||
Text(I18n.CreateLive.totalLabel)
|
Text(I18n.CreateLive.totalLabel)
|
||||||
.appFont(size: 14.7, weight: .medium)
|
.appFont(size: 14.7, weight: .medium)
|
||||||
.foregroundColor(Color.grayee)
|
.foregroundColor(Color.grayee)
|
||||||
@@ -65,10 +65,6 @@ struct LiveRoomHeartRankingDialog: View {
|
|||||||
.foregroundColor(Color.button)
|
.foregroundColor(Color.button)
|
||||||
.padding(.leading, 6.7)
|
.padding(.leading, 6.7)
|
||||||
|
|
||||||
Text(I18n.CreateLive.peopleUnit)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.foregroundColor(Color.gray77)
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.top, 13.3)
|
.padding(.top, 13.3)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ struct BlockMemberListView: View {
|
|||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
DetailNavigationBar(title: I18n.MyPage.Block.listTitle)
|
DetailNavigationBar(title: I18n.MyPage.Block.listTitle)
|
||||||
|
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 4) {
|
||||||
Text(I18n.MyPage.Common.totalPrefix)
|
Text(I18n.MyPage.Common.totalPrefix)
|
||||||
.appFont(size: 13.3, weight: .medium)
|
.appFont(size: 13.3, weight: .medium)
|
||||||
.foregroundColor(Color.grayee)
|
.foregroundColor(Color.grayee)
|
||||||
@@ -25,10 +25,6 @@ struct BlockMemberListView: View {
|
|||||||
.appFont(size: 13.3, weight: .medium)
|
.appFont(size: 13.3, weight: .medium)
|
||||||
.foregroundColor(Color.mainRed3)
|
.foregroundColor(Color.mainRed3)
|
||||||
|
|
||||||
Text(I18n.MyPage.Common.personUnit)
|
|
||||||
.appFont(size: 13.3, weight: .medium)
|
|
||||||
.foregroundColor(Color.grayee)
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 13.3)
|
.padding(.horizontal, 13.3)
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import RefreshableScrollView
|
|||||||
|
|
||||||
struct MyPageView: View {
|
struct MyPageView: View {
|
||||||
|
|
||||||
|
@Environment(\.locale) private var locale
|
||||||
|
|
||||||
@StateObject var viewModel = MyPageViewModel()
|
@StateObject var viewModel = MyPageViewModel()
|
||||||
@StateObject var recentContentViewModel = RecentContentViewModel()
|
@StateObject var recentContentViewModel = RecentContentViewModel()
|
||||||
|
|
||||||
@@ -136,9 +138,10 @@ struct MyPageView: View {
|
|||||||
|
|
||||||
if let url = URL(string: "https://bit.ly/4h7pVks"),
|
if let url = URL(string: "https://bit.ly/4h7pVks"),
|
||||||
UIApplication.shared.canOpenURL(url) {
|
UIApplication.shared.canOpenURL(url) {
|
||||||
// Voice On Banner
|
// SodaLive Banner
|
||||||
Image("img_apply_creator")
|
Image(locale.identifier.hasPrefix("ja") ? "img_apply_creator_ja" : "img_apply_creator")
|
||||||
.resizable()
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.horizontal, 24)
|
.padding(.horizontal, 24)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
@@ -148,9 +151,10 @@ struct MyPageView: View {
|
|||||||
|
|
||||||
if let url = URL(string: "https://blog.naver.com/sodalive_official"),
|
if let url = URL(string: "https://blog.naver.com/sodalive_official"),
|
||||||
UIApplication.shared.canOpenURL(url) {
|
UIApplication.shared.canOpenURL(url) {
|
||||||
// Voice On Banner
|
// SodaLive Banner
|
||||||
Image("img_introduce_voiceon")
|
Image(locale.identifier.hasPrefix("ja") ? "img_introduce_sodalive_ja" : "img_introduce_sodalive")
|
||||||
.resizable()
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.horizontal, 24)
|
.padding(.horizontal, 24)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import Kingfisher
|
|||||||
struct EventListView: View {
|
struct EventListView: View {
|
||||||
|
|
||||||
@ObservedObject var viewModel = EventListViewModel()
|
@ObservedObject var viewModel = EventListViewModel()
|
||||||
|
@State private var hasLoadedEvents = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
@@ -50,6 +51,8 @@ struct EventListView: View {
|
|||||||
}
|
}
|
||||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
guard !hasLoadedEvents else { return }
|
||||||
|
hasLoadedEvents = true
|
||||||
viewModel.getEvents()
|
viewModel.getEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,15 +20,14 @@ struct SplashView: View {
|
|||||||
Color.black
|
Color.black
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 8) {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Text(I18n.Splash.tagline)
|
|
||||||
.appFont(size: 23, weight: .bold)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
|
|
||||||
Image("splash_text_logo")
|
Image("splash_text_logo")
|
||||||
.padding(.top, 10)
|
|
||||||
|
Text(I18n.Splash.tagline)
|
||||||
|
.appFont(.heading4)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,16 +42,16 @@ private struct CapsuleTabBarItem: View {
|
|||||||
} label: {
|
} label: {
|
||||||
Text(title)
|
Text(title)
|
||||||
.appFont(.body5)
|
.appFont(.body5)
|
||||||
.foregroundColor(Color.white)
|
.foregroundColor(isSelected ? Color.black : Color.white)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.padding(.horizontal, SodaSpacing.s12)
|
.padding(.horizontal, SodaSpacing.s12)
|
||||||
.padding(.vertical, SodaSpacing.s8)
|
.padding(.vertical, SodaSpacing.s8)
|
||||||
.frame(height: 34)
|
.frame(height: 34)
|
||||||
.background(isSelected ? Color.soda400 : Color.black)
|
.background(isSelected ? Color.white : Color.black)
|
||||||
.clipShape(Capsule())
|
.clipShape(Capsule())
|
||||||
.overlay(
|
.overlay(
|
||||||
Capsule()
|
Capsule()
|
||||||
.stroke(isSelected ? Color.soda400 : Color.gray700, lineWidth: 1)
|
.stroke(isSelected ? Color.white : Color.gray700, lineWidth: 1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ struct HomeTitleBar<Menu: View>: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TitleBar {
|
TitleBar {
|
||||||
Image("img_text_logo_v2")
|
Image("img_text_logo")
|
||||||
} trailing: {
|
} trailing: {
|
||||||
menu
|
menu
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
import Lottie
|
||||||
|
|
||||||
|
struct LottieRefreshIndicator: View {
|
||||||
|
let isAnimating: Bool
|
||||||
|
|
||||||
|
@Environment(\.accessibilityReduceMotion) private var accessibilityReduceMotion
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let animation = LottieView(animation: .named("pull-to-refresh"))
|
||||||
|
.configure(\.contentMode, to: .scaleAspectFit)
|
||||||
|
.resizable()
|
||||||
|
|
||||||
|
Group {
|
||||||
|
if accessibilityReduceMotion {
|
||||||
|
animation.currentFrame(1)
|
||||||
|
} else if isAnimating {
|
||||||
|
animation.looping()
|
||||||
|
} else {
|
||||||
|
animation.currentFrame(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: SodaSpacing.s48, maxHeight: SodaSpacing.s48)
|
||||||
|
.accessibilityHidden(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LottieRefreshableScrollView<Content: View>: View {
|
||||||
|
private let action: () async -> Void
|
||||||
|
private let content: () -> Content
|
||||||
|
private let refreshRegionHeight = SodaSpacing.s48 + (SodaSpacing.s8 * 2)
|
||||||
|
|
||||||
|
@Environment(\.accessibilityReduceMotion) private var accessibilityReduceMotion
|
||||||
|
@State private var pullOffset: CGFloat = 0
|
||||||
|
@State private var isRefreshArmed = false
|
||||||
|
@State private var isRefreshing = false
|
||||||
|
|
||||||
|
init(
|
||||||
|
action: @escaping () async -> Void,
|
||||||
|
@ViewBuilder content: @escaping () -> Content
|
||||||
|
) {
|
||||||
|
self.action = action
|
||||||
|
self.content = content
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
pullOffsetReader
|
||||||
|
refreshRegion
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.scrollBounceBehavior(.always, axes: .vertical)
|
||||||
|
.background(PullOffsetPositionReader(type: .fixed))
|
||||||
|
.onPreferenceChange(PullOffsetPreferenceKey.self) { positions in
|
||||||
|
guard
|
||||||
|
let movingMinY = positions.first(where: { $0.type == .moving })?.minY,
|
||||||
|
let fixedMinY = positions.first(where: { $0.type == .fixed })?.minY
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
let offset = max(0, movingMinY - fixedMinY)
|
||||||
|
pullOffset = offset
|
||||||
|
|
||||||
|
guard !isRefreshing else { return }
|
||||||
|
|
||||||
|
if offset >= refreshRegionHeight {
|
||||||
|
isRefreshArmed = true
|
||||||
|
} else if isRefreshArmed {
|
||||||
|
isRefreshArmed = false
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.accessibilityAction(named: Text(I18n.LiveNow.refreshButton)) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var pullOffsetReader: some View {
|
||||||
|
PullOffsetPositionReader(type: .moving)
|
||||||
|
.frame(height: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var refreshRegion: some View {
|
||||||
|
Color.clear
|
||||||
|
.frame(height: isRefreshing ? refreshRegionHeight : 0)
|
||||||
|
.overlay(alignment: .top) {
|
||||||
|
if pullOffset > 0 || isRefreshing {
|
||||||
|
LottieRefreshIndicator(isAnimating: true)
|
||||||
|
.padding(.vertical, SodaSpacing.s8)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(height: refreshRegionHeight)
|
||||||
|
.offset(y: isRefreshing ? 0 : -refreshRegionHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refresh() {
|
||||||
|
isRefreshArmed = false
|
||||||
|
guard !isRefreshing else { return }
|
||||||
|
|
||||||
|
withAnimation(accessibilityReduceMotion ? nil : .easeOut(duration: 0.2)) {
|
||||||
|
isRefreshing = true
|
||||||
|
}
|
||||||
|
Task { @MainActor in
|
||||||
|
await action()
|
||||||
|
withAnimation(accessibilityReduceMotion ? nil : .easeOut(duration: 0.2)) {
|
||||||
|
pullOffset = 0
|
||||||
|
isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum PullOffsetPositionType {
|
||||||
|
case fixed
|
||||||
|
case moving
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PullOffsetPosition: Equatable {
|
||||||
|
let type: PullOffsetPositionType
|
||||||
|
let minY: CGFloat
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PullOffsetPreferenceKey: PreferenceKey {
|
||||||
|
static var defaultValue: [PullOffsetPosition] = []
|
||||||
|
|
||||||
|
static func reduce(value: inout [PullOffsetPosition], nextValue: () -> [PullOffsetPosition]) {
|
||||||
|
value.append(contentsOf: nextValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PullOffsetPositionReader: View {
|
||||||
|
let type: PullOffsetPositionType
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GeometryReader { proxy in
|
||||||
|
Color.clear.preference(
|
||||||
|
key: PullOffsetPreferenceKey.self,
|
||||||
|
value: [PullOffsetPosition(type: type, minY: proxy.frame(in: .global).minY)]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ struct TextTabBar<Item: Hashable>: View {
|
|||||||
let title: (Item) -> String
|
let title: (Item) -> String
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
ScrollView(.horizontal) {
|
||||||
HStack(alignment: .center, spacing: SodaSpacing.s20) {
|
HStack(alignment: .center, spacing: SodaSpacing.s20) {
|
||||||
ForEach(items.prefix(3), id: \.self) { item in
|
ForEach(items.prefix(3), id: \.self) { item in
|
||||||
Button {
|
Button {
|
||||||
@@ -19,15 +20,16 @@ struct TextTabBar<Item: Hashable>: View {
|
|||||||
Text(title(item))
|
Text(title(item))
|
||||||
.appFont(.heading3)
|
.appFont(.heading3)
|
||||||
.foregroundColor(selectedItem == item ? Color.white : Color.gray600)
|
.foregroundColor(selectedItem == item ? Color.white : Color.gray600)
|
||||||
|
.fixedSize(horizontal: true, vertical: false)
|
||||||
.frame(height: 52, alignment: .center)
|
.frame(height: 52, alignment: .center)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, SodaSpacing.s20)
|
.padding(.horizontal, SodaSpacing.s20)
|
||||||
|
}
|
||||||
|
.scrollIndicators(.hidden)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.frame(height: 52, alignment: .leading)
|
.frame(height: 52, alignment: .leading)
|
||||||
.background(Color.black)
|
.background(Color.black)
|
||||||
|
|||||||
@@ -3,26 +3,22 @@ import SwiftUI
|
|||||||
struct CreatorChannelCommunityGridItem: View {
|
struct CreatorChannelCommunityGridItem: View {
|
||||||
let post: CreatorChannelCommunityPostItem
|
let post: CreatorChannelCommunityPostItem
|
||||||
let isOwnPost: Bool
|
let isOwnPost: Bool
|
||||||
let onTapPurchase: () -> Void
|
|
||||||
let onTapDetail: () -> Void
|
let onTapDetail: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { proxy in
|
GeometryReader { proxy in
|
||||||
ZStack {
|
ZStack {
|
||||||
if isPaidLocked {
|
if isPaidLocked {
|
||||||
Button(action: onTapPurchase) {
|
Color.gray800
|
||||||
Color.gray400
|
|
||||||
.overlay(paidPriceOverlay)
|
.overlay(paidPriceOverlay)
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
} else {
|
} else {
|
||||||
tileContent(proxy: proxy)
|
tileContent(proxy: proxy)
|
||||||
.contentShape(Rectangle())
|
|
||||||
.onTapGesture(perform: onTapDetail)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: proxy.size.width, height: proxy.size.width)
|
.frame(width: proxy.size.width, height: proxy.size.width)
|
||||||
.clipped()
|
.clipped()
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture(perform: onTapDetail)
|
||||||
.overlay(alignment: .topTrailing) {
|
.overlay(alignment: .topTrailing) {
|
||||||
if post.isPinned {
|
if post.isPinned {
|
||||||
Image("ic_pin")
|
Image("ic_pin")
|
||||||
@@ -92,3 +88,32 @@ struct CreatorChannelCommunityGridItem: View {
|
|||||||
return String(content.prefix(18))
|
return String(content.prefix(18))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct CreatorChannelCommunityGridItem_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
CreatorChannelCommunityGridItem(
|
||||||
|
post: CreatorChannelCommunityPostItem(
|
||||||
|
postId: 1,
|
||||||
|
creatorId: 10,
|
||||||
|
creatorNickname: "크리에이터 이름",
|
||||||
|
creatorProfileUrl: "https://picsum.photos/600",
|
||||||
|
createdAtUtc: "2026-08-10T00:00:00Z",
|
||||||
|
content: "유료 커뮤니티 게시글",
|
||||||
|
imageUrl: nil,
|
||||||
|
audioUrl: nil,
|
||||||
|
price: 30,
|
||||||
|
isCommentAvailable: true,
|
||||||
|
likeCount: 5,
|
||||||
|
commentCount: 2,
|
||||||
|
isPinned: true,
|
||||||
|
existOrdered: false,
|
||||||
|
isLiked: false
|
||||||
|
),
|
||||||
|
isOwnPost: false,
|
||||||
|
onTapDetail: {}
|
||||||
|
)
|
||||||
|
.frame(width: 134, height: 134)
|
||||||
|
.background(Color.black)
|
||||||
|
.previewLayout(.sizeThatFits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ struct CreatorChannelCommunityListItem: View {
|
|||||||
let isOwnCreatorChannel: Bool
|
let isOwnCreatorChannel: Bool
|
||||||
let onTapLike: () -> Void
|
let onTapLike: () -> Void
|
||||||
let onTapMore: () -> Void
|
let onTapMore: () -> Void
|
||||||
let onTapPurchase: () -> Void
|
|
||||||
let onTapDetail: () -> Void
|
let onTapDetail: () -> Void
|
||||||
|
|
||||||
@State private var localIsLike: Bool
|
@State private var localIsLike: Bool
|
||||||
@@ -23,7 +22,6 @@ struct CreatorChannelCommunityListItem: View {
|
|||||||
isOwnCreatorChannel: Bool,
|
isOwnCreatorChannel: Bool,
|
||||||
onTapLike: @escaping () -> Void,
|
onTapLike: @escaping () -> Void,
|
||||||
onTapMore: @escaping () -> Void,
|
onTapMore: @escaping () -> Void,
|
||||||
onTapPurchase: @escaping () -> Void,
|
|
||||||
onTapDetail: @escaping () -> Void
|
onTapDetail: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
self.post = post
|
self.post = post
|
||||||
@@ -31,7 +29,6 @@ struct CreatorChannelCommunityListItem: View {
|
|||||||
self.isOwnCreatorChannel = isOwnCreatorChannel
|
self.isOwnCreatorChannel = isOwnCreatorChannel
|
||||||
self.onTapLike = onTapLike
|
self.onTapLike = onTapLike
|
||||||
self.onTapMore = onTapMore
|
self.onTapMore = onTapMore
|
||||||
self.onTapPurchase = onTapPurchase
|
|
||||||
self.onTapDetail = onTapDetail
|
self.onTapDetail = onTapDetail
|
||||||
_localIsLike = State(initialValue: post.isLiked ?? false)
|
_localIsLike = State(initialValue: post.isLiked ?? false)
|
||||||
_localLikeCount = State(initialValue: post.likeCount)
|
_localLikeCount = State(initialValue: post.likeCount)
|
||||||
@@ -53,8 +50,10 @@ struct CreatorChannelCommunityListItem: View {
|
|||||||
imageContent(imageUrl: imageUrl)
|
imageContent(imageUrl: imageUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isPaidLocked {
|
||||||
reactionBar
|
reactionBar
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(SodaSpacing.s14)
|
.padding(SodaSpacing.s14)
|
||||||
.background(Color.gray900)
|
.background(Color.gray900)
|
||||||
@@ -113,7 +112,7 @@ struct CreatorChannelCommunityListItem: View {
|
|||||||
|
|
||||||
moreButton
|
moreButton
|
||||||
}
|
}
|
||||||
} else if !isOwnPost {
|
} else if !isPaidLocked {
|
||||||
moreButton
|
moreButton
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,15 +171,12 @@ struct CreatorChannelCommunityListItem: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var paidLockedImage: some View {
|
private var paidLockedImage: some View {
|
||||||
Button(action: onTapPurchase) {
|
|
||||||
RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)
|
RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)
|
||||||
.fill(Color.gray400)
|
.fill(Color.gray800)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.frame(height: 236)
|
.frame(height: 236)
|
||||||
.overlay(paidPriceOverlay)
|
.overlay(paidPriceOverlay)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var paidPriceOverlay: some View {
|
private var paidPriceOverlay: some View {
|
||||||
VStack(spacing: SodaSpacing.s4) {
|
VStack(spacing: SodaSpacing.s4) {
|
||||||
@@ -219,7 +215,7 @@ struct CreatorChannelCommunityListItem: View {
|
|||||||
|
|
||||||
private var reactionBar: some View {
|
private var reactionBar: some View {
|
||||||
HStack(spacing: 15) {
|
HStack(spacing: 15) {
|
||||||
if post.isCommentAvailable && !isPaidLocked {
|
if post.isCommentAvailable {
|
||||||
HStack(spacing: SodaSpacing.s4) {
|
HStack(spacing: SodaSpacing.s4) {
|
||||||
Image("ic_feed_community_reply")
|
Image("ic_feed_community_reply")
|
||||||
.resizable()
|
.resizable()
|
||||||
@@ -231,7 +227,6 @@ struct CreatorChannelCommunityListItem: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isPaidLocked {
|
|
||||||
Button {
|
Button {
|
||||||
localIsLike.toggle()
|
localIsLike.toggle()
|
||||||
localLikeCount += localIsLike ? 1 : -1
|
localLikeCount += localIsLike ? 1 : -1
|
||||||
@@ -250,7 +245,6 @@ struct CreatorChannelCommunityListItem: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.frame(height: 24)
|
.frame(height: 24)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ struct CreatorChannelCommunityViewModeBar: View {
|
|||||||
|
|
||||||
Button(action: onTapToggle) {
|
Button(action: onTapToggle) {
|
||||||
HStack(spacing: SodaSpacing.s4) {
|
HStack(spacing: SodaSpacing.s4) {
|
||||||
|
if LanguageHeaderProvider.current != "ja" {
|
||||||
Text(viewModeTitle)
|
Text(viewModeTitle)
|
||||||
.appFont(size: 16, weight: .regular)
|
.appFont(size: 16, weight: .regular)
|
||||||
.foregroundColor(Color.gray400)
|
.foregroundColor(Color.gray400)
|
||||||
|
}
|
||||||
|
|
||||||
Image(viewModeIconName)
|
Image(viewModeIconName)
|
||||||
.resizable()
|
.resizable()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ struct CreatorChannelCommunityTabView: View {
|
|||||||
let creatorId: Int
|
let creatorId: Int
|
||||||
let isOwnCreatorChannel: Bool
|
let isOwnCreatorChannel: Bool
|
||||||
@ObservedObject var viewModel: CreatorChannelCommunityViewModel
|
@ObservedObject var viewModel: CreatorChannelCommunityViewModel
|
||||||
let onTapDetail: (CreatorChannelCommunityPostItem) -> Void
|
let onTapDetail: (Int) -> Void
|
||||||
|
|
||||||
private let gridColumns = [
|
private let gridColumns = [
|
||||||
GridItem(.flexible(), spacing: 0),
|
GridItem(.flexible(), spacing: 0),
|
||||||
@@ -69,11 +69,8 @@ struct CreatorChannelCommunityTabView: View {
|
|||||||
onTapMore: {
|
onTapMore: {
|
||||||
viewModel.openReportMenu(post: post)
|
viewModel.openReportMenu(post: post)
|
||||||
},
|
},
|
||||||
onTapPurchase: {
|
|
||||||
viewModel.openPurchaseDialog(post: post)
|
|
||||||
},
|
|
||||||
onTapDetail: {
|
onTapDetail: {
|
||||||
onTapDetail(post)
|
onTapDetail(post.postId)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
@@ -95,11 +92,8 @@ struct CreatorChannelCommunityTabView: View {
|
|||||||
CreatorChannelCommunityGridItem(
|
CreatorChannelCommunityGridItem(
|
||||||
post: post,
|
post: post,
|
||||||
isOwnPost: isOwnPost,
|
isOwnPost: isOwnPost,
|
||||||
onTapPurchase: {
|
|
||||||
viewModel.openPurchaseDialog(post: post)
|
|
||||||
},
|
|
||||||
onTapDetail: {
|
onTapDetail: {
|
||||||
onTapDetail(post)
|
onTapDetail(post.postId)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
|||||||
@@ -50,16 +50,7 @@ final class CreatorChannelCommunityViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Published var isShowPostPurchaseView = false {
|
|
||||||
didSet {
|
|
||||||
if !isShowPostPurchaseView {
|
|
||||||
selectedPostId = 0
|
|
||||||
selectedPostPrice = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Published var selectedPostId = 0
|
@Published var selectedPostId = 0
|
||||||
@Published var selectedPostPrice = 0
|
|
||||||
@Published var selectedPostIsPinned = false
|
@Published var selectedPostIsPinned = false
|
||||||
@Published var isShowSecret = false
|
@Published var isShowSecret = false
|
||||||
|
|
||||||
@@ -94,17 +85,10 @@ final class CreatorChannelCommunityViewModel: ObservableObject {
|
|||||||
|
|
||||||
func openReportMenu(post: CreatorChannelCommunityPostItem) {
|
func openReportMenu(post: CreatorChannelCommunityPostItem) {
|
||||||
selectedPostId = post.postId
|
selectedPostId = post.postId
|
||||||
selectedPostPrice = post.price
|
|
||||||
selectedPostIsPinned = post.isPinned
|
selectedPostIsPinned = post.isPinned
|
||||||
isShowReportMenu = true
|
isShowReportMenu = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func openPurchaseDialog(post: CreatorChannelCommunityPostItem) {
|
|
||||||
selectedPostId = post.postId
|
|
||||||
selectedPostPrice = post.price
|
|
||||||
isShowPostPurchaseView = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func communityPostLike(postId: Int) {
|
func communityPostLike(postId: Int) {
|
||||||
communityRepository.communityPostLike(postId: postId)
|
communityRepository.communityPostLike(postId: postId)
|
||||||
.sink { result in
|
.sink { result in
|
||||||
@@ -153,7 +137,7 @@ final class CreatorChannelCommunityViewModel: ObservableObject {
|
|||||||
.store(in: &subscription)
|
.store(in: &subscription)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteCommunityPost(creatorId: Int) {
|
func deleteCommunityPost(onSuccess: @escaping () -> Void) {
|
||||||
guard selectedPostId > 0 else { return }
|
guard selectedPostId > 0 else { return }
|
||||||
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
@@ -190,7 +174,7 @@ final class CreatorChannelCommunityViewModel: ObservableObject {
|
|||||||
if decoded.success {
|
if decoded.success {
|
||||||
self.errorMessage = I18n.Explorer.deleted
|
self.errorMessage = I18n.Explorer.deleted
|
||||||
self.isShowPopup = true
|
self.isShowPopup = true
|
||||||
self.fetchFirstPage(creatorId: creatorId)
|
onSuccess()
|
||||||
} else {
|
} else {
|
||||||
self.errorMessage = decoded.message ?? I18n.Common.commonError
|
self.errorMessage = decoded.message ?? I18n.Common.commonError
|
||||||
self.isShowPopup = true
|
self.isShowPopup = true
|
||||||
@@ -204,7 +188,7 @@ final class CreatorChannelCommunityViewModel: ObservableObject {
|
|||||||
.store(in: &subscription)
|
.store(in: &subscription)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateCommunityPostFixed(creatorId: Int) {
|
func updateCommunityPostFixed(onSuccess: @escaping () -> Void) {
|
||||||
guard selectedPostId > 0 else { return }
|
guard selectedPostId > 0 else { return }
|
||||||
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
@@ -228,7 +212,7 @@ final class CreatorChannelCommunityViewModel: ObservableObject {
|
|||||||
do {
|
do {
|
||||||
let decoded = try JSONDecoder().decode(ApiResponseWithoutData.self, from: response.data)
|
let decoded = try JSONDecoder().decode(ApiResponseWithoutData.self, from: response.data)
|
||||||
if decoded.success {
|
if decoded.success {
|
||||||
self.fetchFirstPage(creatorId: creatorId)
|
onSuccess()
|
||||||
} else {
|
} else {
|
||||||
self.errorMessage = decoded.message ?? I18n.Common.commonError
|
self.errorMessage = decoded.message ?? I18n.Common.commonError
|
||||||
self.isShowPopup = true
|
self.isShowPopup = true
|
||||||
@@ -242,43 +226,6 @@ final class CreatorChannelCommunityViewModel: ObservableObject {
|
|||||||
.store(in: &subscription)
|
.store(in: &subscription)
|
||||||
}
|
}
|
||||||
|
|
||||||
func purchaseCommunityPost(creatorId: Int) {
|
|
||||||
guard selectedPostId > 0 else { return }
|
|
||||||
|
|
||||||
isLoading = true
|
|
||||||
communityRepository.purchaseCommunityPost(postId: selectedPostId)
|
|
||||||
.sink { [weak self] result in
|
|
||||||
guard let self else { return }
|
|
||||||
|
|
||||||
switch result {
|
|
||||||
case .finished:
|
|
||||||
DEBUG_LOG("finish")
|
|
||||||
case .failure(let error):
|
|
||||||
ERROR_LOG(error.localizedDescription)
|
|
||||||
self.applyFailureState()
|
|
||||||
}
|
|
||||||
} receiveValue: { [weak self] response in
|
|
||||||
guard let self else { return }
|
|
||||||
|
|
||||||
self.isLoading = false
|
|
||||||
self.selectedPostId = 0
|
|
||||||
|
|
||||||
do {
|
|
||||||
let decoded = try JSONDecoder().decode(ApiResponseWithoutData.self, from: response.data)
|
|
||||||
if decoded.success {
|
|
||||||
self.fetchFirstPage(creatorId: creatorId)
|
|
||||||
} else {
|
|
||||||
self.errorMessage = decoded.message ?? I18n.Common.commonError
|
|
||||||
self.isShowPopup = true
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
ERROR_LOG(error.localizedDescription)
|
|
||||||
self.fetchFirstPage(creatorId: creatorId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &subscription)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func fetchCommunity(creatorId: Int, page requestPage: Int, isNextPage: Bool) {
|
private func fetchCommunity(creatorId: Int, page requestPage: Int, isNextPage: Bool) {
|
||||||
latestRequestId += 1
|
latestRequestId += 1
|
||||||
let requestId = latestRequestId
|
let requestId = latestRequestId
|
||||||
|
|||||||
@@ -25,8 +25,10 @@ struct CreatorChannelCommunityPostDetailContentView: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
mediaContent
|
mediaContent
|
||||||
|
if !isPaidLocked {
|
||||||
reactionBar
|
reactionBar
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.padding(SodaSpacing.s14)
|
.padding(SodaSpacing.s14)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.background(Color.black)
|
.background(Color.black)
|
||||||
@@ -65,10 +67,6 @@ struct CreatorChannelCommunityPostDetailContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
|
|
||||||
if detail.price > 0 && detail.existOrdered && isOwnPost == false {
|
|
||||||
purchaseCompleteTag
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,19 +140,8 @@ struct CreatorChannelCommunityPostDetailContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var purchaseCompleteTag: some View {
|
|
||||||
Text("구매완료")
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.foregroundColor(Color.black)
|
|
||||||
.padding(.horizontal, SodaSpacing.s8)
|
|
||||||
.padding(.vertical, SodaSpacing.s4)
|
|
||||||
.background(Color.soda400)
|
|
||||||
.clipShape(Capsule())
|
|
||||||
}
|
|
||||||
|
|
||||||
private var reactionBar: some View {
|
private var reactionBar: some View {
|
||||||
HStack(spacing: 15) {
|
HStack(spacing: 15) {
|
||||||
if isPaidLocked == false {
|
|
||||||
if detail.isCommentAvailable {
|
if detail.isCommentAvailable {
|
||||||
HStack(spacing: SodaSpacing.s4) {
|
HStack(spacing: SodaSpacing.s4) {
|
||||||
Image("ic_feed_community_reply")
|
Image("ic_feed_community_reply")
|
||||||
@@ -180,7 +167,6 @@ struct CreatorChannelCommunityPostDetailContentView: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.frame(height: 24)
|
.frame(height: 24)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ struct CreatorChannelCommunityPostDetailView: View {
|
|||||||
CreatorChannelCommunityPostDetailContentView(
|
CreatorChannelCommunityPostDetailContentView(
|
||||||
detail: detail,
|
detail: detail,
|
||||||
onTapLike: viewModel.toggleLike,
|
onTapLike: viewModel.toggleLike,
|
||||||
onTapPurchase: showPurchaseError
|
onTapPurchase: viewModel.presentPurchaseDialog
|
||||||
)
|
)
|
||||||
|
|
||||||
if detail.isCommentAvailable {
|
if viewModel.isCommentInteractionAvailable {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color.gray800)
|
.fill(Color.gray800)
|
||||||
.frame(height: 1)
|
.frame(height: 1)
|
||||||
@@ -69,13 +69,21 @@ struct CreatorChannelCommunityPostDetailView: View {
|
|||||||
onDimmedTap: dismissDeleteDialog
|
onDimmedTap: dismissDeleteDialog
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if viewModel.isShowPurchaseDialog, let detail = viewModel.detail {
|
||||||
|
CommunityPostPurchaseDialog(
|
||||||
|
isShowing: $viewModel.isShowPurchaseDialog,
|
||||||
|
can: detail.price,
|
||||||
|
confirmAction: viewModel.purchaseCommunityPost
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.navigationBarBackButtonHidden(true)
|
.navigationBarBackButtonHidden(true)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.configure(postId: postId, onCommunityRefresh: onCommunityRefresh)
|
viewModel.configure(postId: postId, onCommunityRefresh: onCommunityRefresh)
|
||||||
}
|
}
|
||||||
.safeAreaInset(edge: .bottom) {
|
.safeAreaInset(edge: .bottom) {
|
||||||
if viewModel.detail?.isCommentAvailable == true {
|
if viewModel.isCommentInteractionAvailable {
|
||||||
CreatorChannelCommunityPostDetailInputBar(
|
CreatorChannelCommunityPostDetailInputBar(
|
||||||
text: $viewModel.commentText,
|
text: $viewModel.commentText,
|
||||||
isSecret: $viewModel.isSecret,
|
isSecret: $viewModel.isSecret,
|
||||||
@@ -112,9 +120,4 @@ struct CreatorChannelCommunityPostDetailView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showPurchaseError() {
|
|
||||||
viewModel.errorMessage = I18n.Common.commonError
|
|
||||||
viewModel.isShowPopup = true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ final class CreatorChannelCommunityPostDetailViewModel: ObservableObject {
|
|||||||
@Published var editingComment: CreatorChannelCommunityCommentResponse?
|
@Published var editingComment: CreatorChannelCommunityCommentResponse?
|
||||||
@Published var deletingComment: CreatorChannelCommunityCommentResponse?
|
@Published var deletingComment: CreatorChannelCommunityCommentResponse?
|
||||||
@Published var isShowDeleteDialog = false
|
@Published var isShowDeleteDialog = false
|
||||||
|
@Published var isShowPurchaseDialog = false
|
||||||
@Published var page = 0
|
@Published var page = 0
|
||||||
@Published var size = 20
|
@Published var size = 20
|
||||||
@Published var hasNext = false
|
@Published var hasNext = false
|
||||||
@@ -32,13 +33,18 @@ final class CreatorChannelCommunityPostDetailViewModel: ObservableObject {
|
|||||||
detail?.creatorId == UserDefaults.int(forKey: .userId)
|
detail?.creatorId == UserDefaults.int(forKey: .userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isCommentInteractionAvailable: Bool {
|
||||||
|
guard let detail, detail.isCommentAvailable else { return false }
|
||||||
|
return detail.price <= 0 || detail.existOrdered || isOwnPost
|
||||||
|
}
|
||||||
|
|
||||||
var isShowSecret: Bool {
|
var isShowSecret: Bool {
|
||||||
guard let detail else { return false }
|
guard let detail else { return false }
|
||||||
return detail.price > 0 && detail.existOrdered && detail.creatorId != UserDefaults.int(forKey: .userId)
|
return detail.price > 0 && detail.existOrdered && detail.creatorId != UserDefaults.int(forKey: .userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
var isCommentSendEnabled: Bool {
|
var isCommentSendEnabled: Bool {
|
||||||
guard detail?.isCommentAvailable == true else { return false }
|
guard isCommentInteractionAvailable else { return false }
|
||||||
return commentText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false && isLoading == false
|
return commentText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false && isLoading == false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +107,7 @@ final class CreatorChannelCommunityPostDetailViewModel: ObservableObject {
|
|||||||
|
|
||||||
func fetchNextCommentsIfNeeded(currentComment: CreatorChannelCommunityCommentResponse) {
|
func fetchNextCommentsIfNeeded(currentComment: CreatorChannelCommunityCommentResponse) {
|
||||||
guard comments.last?.commentId == currentComment.commentId else { return }
|
guard comments.last?.commentId == currentComment.commentId else { return }
|
||||||
guard detail?.isCommentAvailable == true else { return }
|
guard isCommentInteractionAvailable else { return }
|
||||||
guard hasNext, isLoadingNextPage == false, isLoading == false else { return }
|
guard hasNext, isLoadingNextPage == false, isLoading == false else { return }
|
||||||
guard postId > 0 else { return }
|
guard postId > 0 else { return }
|
||||||
|
|
||||||
@@ -182,8 +188,51 @@ final class CreatorChannelCommunityPostDetailViewModel: ObservableObject {
|
|||||||
.store(in: &subscription)
|
.store(in: &subscription)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func presentPurchaseDialog() {
|
||||||
|
guard let detail, detail.price > 0, detail.existOrdered == false, isOwnPost == false else { return }
|
||||||
|
isShowPurchaseDialog = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func purchaseCommunityPost() {
|
||||||
|
guard postId > 0, isLoading == false else { return }
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
communityRepository.purchaseCommunityPost(postId: postId)
|
||||||
|
.sink { [weak self] result in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .finished:
|
||||||
|
DEBUG_LOG("finish")
|
||||||
|
case .failure(let error):
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
self.applyFailureState()
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] response in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
self.isLoading = false
|
||||||
|
|
||||||
|
do {
|
||||||
|
let decoded = try JSONDecoder().decode(ApiResponseWithoutData.self, from: response.data)
|
||||||
|
if decoded.success {
|
||||||
|
self.onCommunityRefresh?()
|
||||||
|
self.fetchDetail(postId: self.postId)
|
||||||
|
} else {
|
||||||
|
self.errorMessage = decoded.message ?? I18n.Common.commonError
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
self.errorMessage = I18n.Common.commonError
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
}
|
||||||
|
|
||||||
func sendComment() {
|
func sendComment() {
|
||||||
guard isCommentSendEnabled else { return }
|
guard isCommentInteractionAvailable, isCommentSendEnabled else { return }
|
||||||
|
|
||||||
if let editingComment {
|
if let editingComment {
|
||||||
modify(comment: editingComment)
|
modify(comment: editingComment)
|
||||||
@@ -209,7 +258,7 @@ final class CreatorChannelCommunityPostDetailViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func beginEditing(_ comment: CreatorChannelCommunityCommentResponse) {
|
func beginEditing(_ comment: CreatorChannelCommunityCommentResponse) {
|
||||||
guard canEdit(comment) else { return }
|
guard isCommentInteractionAvailable, canEdit(comment) else { return }
|
||||||
selectedComment = nil
|
selectedComment = nil
|
||||||
editingComment = comment
|
editingComment = comment
|
||||||
commentText = comment.content
|
commentText = comment.content
|
||||||
@@ -221,14 +270,14 @@ final class CreatorChannelCommunityPostDetailViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func presentDeleteDialog(_ comment: CreatorChannelCommunityCommentResponse) {
|
func presentDeleteDialog(_ comment: CreatorChannelCommunityCommentResponse) {
|
||||||
guard canDelete(comment) else { return }
|
guard isCommentInteractionAvailable, canDelete(comment) else { return }
|
||||||
selectedComment = nil
|
selectedComment = nil
|
||||||
deletingComment = comment
|
deletingComment = comment
|
||||||
isShowDeleteDialog = true
|
isShowDeleteDialog = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func confirmDelete() {
|
func confirmDelete() {
|
||||||
guard let deletingComment, isLoading == false else { return }
|
guard isCommentInteractionAvailable, let deletingComment, isLoading == false else { return }
|
||||||
isShowDeleteDialog = false
|
isShowDeleteDialog = false
|
||||||
delete(comment: deletingComment)
|
delete(comment: deletingComment)
|
||||||
}
|
}
|
||||||
@@ -240,7 +289,7 @@ final class CreatorChannelCommunityPostDetailViewModel: ObservableObject {
|
|||||||
hasNext = data.comments.hasNext
|
hasNext = data.comments.hasNext
|
||||||
hasLoaded = true
|
hasLoaded = true
|
||||||
|
|
||||||
if data.isCommentAvailable {
|
if isCommentInteractionAvailable {
|
||||||
comments = data.comments.comments
|
comments = data.comments.comments
|
||||||
} else {
|
} else {
|
||||||
comments = []
|
comments = []
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ struct CreatorChannelTabBar: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
proxy.scrollTo(selectedTab, anchor: .center)
|
||||||
|
}
|
||||||
.onChange(of: selectedTab) { tab in
|
.onChange(of: selectedTab) { tab in
|
||||||
withAnimation {
|
withAnimation {
|
||||||
proxy.scrollTo(tab, anchor: .center)
|
proxy.scrollTo(tab, anchor: .center)
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ struct CreatorChannelView: View {
|
|||||||
@State private var isShowChannelDonationDialog = false
|
@State private var isShowChannelDonationDialog = false
|
||||||
@State private var isShowAuthView = false
|
@State private var isShowAuthView = false
|
||||||
@State private var isShowAuthConfirmView = false
|
@State private var isShowAuthConfirmView = false
|
||||||
|
@State private var isShowReportMenu = false
|
||||||
|
@State private var isShowUserBlockConfirm = false
|
||||||
|
@State private var isShowUserReport = false
|
||||||
|
@State private var isShowProfileReport = false
|
||||||
|
@State private var isShowUnfollowConfirmDialog = false
|
||||||
@State private var isCreatorActionMenuPresented = false
|
@State private var isCreatorActionMenuPresented = false
|
||||||
@State private var pendingAction: (() -> Void)? = nil
|
@State private var pendingAction: (() -> Void)? = nil
|
||||||
@State private var payload = Payload()
|
@State private var payload = Payload()
|
||||||
@@ -160,6 +165,10 @@ struct CreatorChannelView: View {
|
|||||||
fanTalkDeleteDialog
|
fanTalkDeleteDialog
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isShowUnfollowConfirmDialog {
|
||||||
|
unfollowConfirmDialog
|
||||||
|
}
|
||||||
|
|
||||||
if isShowChannelDonationDialog {
|
if isShowChannelDonationDialog {
|
||||||
LiveRoomDonationDialogView(
|
LiveRoomDonationDialogView(
|
||||||
isShowing: $isShowChannelDonationDialog,
|
isShowing: $isShowChannelDonationDialog,
|
||||||
@@ -183,6 +192,56 @@ struct CreatorChannelView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if viewModel.response != nil && isShowReportMenu {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
ProfileReportMenuView(
|
||||||
|
isShowing: $isShowReportMenu,
|
||||||
|
isBlockedUser: false,
|
||||||
|
userBlockAction: { isShowUserBlockConfirm = true },
|
||||||
|
userUnBlockAction: {},
|
||||||
|
userReportAction: { isShowUserReport = true },
|
||||||
|
profileReportAction: { isShowProfileReport = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
if proxy.safeAreaInsets.bottom > 0 {
|
||||||
|
Rectangle()
|
||||||
|
.foregroundColor(Color(hex: "222222"))
|
||||||
|
.frame(width: proxy.size.width, height: 15.3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ignoresSafeArea()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let creator = viewModel.response?.creator, isShowUserBlockConfirm {
|
||||||
|
UserBlockConfirmDialogView(
|
||||||
|
isShowing: $isShowUserBlockConfirm,
|
||||||
|
nickname: creator.nickname,
|
||||||
|
confirmAction: {
|
||||||
|
viewModel.userBlock(userId: creator.creatorId) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isShowUserReport {
|
||||||
|
UserReportDialogView(
|
||||||
|
isShowing: $isShowUserReport,
|
||||||
|
confirmAction: { reason in
|
||||||
|
viewModel.report(type: .USER, userId: creatorId, reason: reason)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isShowProfileReport {
|
||||||
|
ProfileReportDialogView(
|
||||||
|
isShowing: $isShowProfileReport,
|
||||||
|
confirmAction: {
|
||||||
|
viewModel.report(type: .PROFILE, userId: creatorId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
communityOverlay(proxy: proxy)
|
communityOverlay(proxy: proxy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,7 +278,7 @@ struct CreatorChannelView: View {
|
|||||||
viewModel.creatorFollow(follow: true, notify: true)
|
viewModel.creatorFollow(follow: true, notify: true)
|
||||||
},
|
},
|
||||||
onTapUnfollow: {
|
onTapUnfollow: {
|
||||||
viewModel.creatorFollow(follow: false, notify: false)
|
isShowUnfollowConfirmDialog = true
|
||||||
},
|
},
|
||||||
onTapNotify: {
|
onTapNotify: {
|
||||||
viewModel.creatorFollow(follow: true, notify: true)
|
viewModel.creatorFollow(follow: true, notify: true)
|
||||||
@@ -227,7 +286,9 @@ struct CreatorChannelView: View {
|
|||||||
onTapUnnotify: {
|
onTapUnnotify: {
|
||||||
viewModel.creatorFollow(follow: true, notify: false)
|
viewModel.creatorFollow(follow: true, notify: false)
|
||||||
},
|
},
|
||||||
onTapMore: {}
|
onTapMore: {
|
||||||
|
isShowReportMenu = true
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,13 +382,17 @@ struct CreatorChannelView: View {
|
|||||||
} else if let response = viewModel.response {
|
} else if let response = viewModel.response {
|
||||||
CreatorChannelHomeView(
|
CreatorChannelHomeView(
|
||||||
response: response,
|
response: response,
|
||||||
|
isOwnCreatorChannel: isOwnCreatorChannel,
|
||||||
onSelectTab: selectTab,
|
onSelectTab: selectTab,
|
||||||
onTapLive: showLiveDetail,
|
onTapLive: showLiveDetail,
|
||||||
onTapContent: showContentDetail,
|
onTapContent: showContentDetail,
|
||||||
onTapDonate: showDonationDialog,
|
onTapDonate: showDonationDialog,
|
||||||
|
onTapFanTalkWrite: showFanTalkWriteView,
|
||||||
onTapSchedule: handleScheduleTap,
|
onTapSchedule: handleScheduleTap,
|
||||||
onTapSeries: showSeriesDetail,
|
onTapSeries: showSeriesDetail,
|
||||||
onTapCommunityLike: viewModel.likeCommunityPost
|
onTapCommunityLike: viewModel.likeCommunityPost,
|
||||||
|
onTapCommunityMore: communityViewModel.openReportMenu,
|
||||||
|
onTapCommunityDetail: showCommunityPostDetail
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
@@ -383,6 +448,31 @@ struct CreatorChannelView: View {
|
|||||||
fanTalkViewModel.selectedFanTalkId = 0
|
fanTalkViewModel.selectedFanTalkId = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var unfollowConfirmDialog: some View {
|
||||||
|
SodaV2ActionModal(
|
||||||
|
title: I18n.MemberChannel.unfollowActionModalTitle,
|
||||||
|
message: I18n.MemberChannel.unfollowActionModalMessage(viewModel.response?.creator.nickname ?? ""),
|
||||||
|
button1: SodaV2ActionModalButton(
|
||||||
|
label: I18n.MemberChannel.unfollowActionModalConfirm,
|
||||||
|
action: confirmUnfollow
|
||||||
|
),
|
||||||
|
button2: SodaV2ActionModalButton(
|
||||||
|
label: I18n.MemberChannel.unfollowActionModalCancel,
|
||||||
|
action: dismissUnfollowConfirmDialog
|
||||||
|
),
|
||||||
|
onDimmedTap: dismissUnfollowConfirmDialog
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func confirmUnfollow() {
|
||||||
|
isShowUnfollowConfirmDialog = false
|
||||||
|
viewModel.creatorFollow(follow: false, notify: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func dismissUnfollowConfirmDialog() {
|
||||||
|
isShowUnfollowConfirmDialog = false
|
||||||
|
}
|
||||||
|
|
||||||
private var authView: some View {
|
private var authView: some View {
|
||||||
BootpayUI(payload: payload, requestType: BootpayRequest.TYPE_AUTHENTICATION)
|
BootpayUI(payload: payload, requestType: BootpayRequest.TYPE_AUTHENTICATION)
|
||||||
.onConfirm { _ in true }
|
.onConfirm { _ in true }
|
||||||
@@ -474,17 +564,22 @@ struct CreatorChannelView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showCommunityPostDetail(_ post: CreatorChannelCommunityPostItem) {
|
private func showCommunityPostDetail(_ postId: Int) {
|
||||||
AppState.shared.setAppStep(
|
AppState.shared.setAppStep(
|
||||||
step: .creatorChannelCommunityPostDetail(
|
step: .creatorChannelCommunityPostDetail(
|
||||||
postId: post.postId,
|
postId: postId,
|
||||||
onCommunityRefresh: {
|
onCommunityRefresh: {
|
||||||
communityViewModel.fetchFirstPage(creatorId: creatorId)
|
refreshCommunityFeeds()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func refreshCommunityFeeds() {
|
||||||
|
viewModel.fetchHome(creatorId: creatorId)
|
||||||
|
communityViewModel.fetchFirstPage(creatorId: creatorId)
|
||||||
|
}
|
||||||
|
|
||||||
private func showAudioContentUpload() {
|
private func showAudioContentUpload() {
|
||||||
isCreatorActionMenuPresented = false
|
isCreatorActionMenuPresented = false
|
||||||
AppState.shared.setAppStep(
|
AppState.shared.setAppStep(
|
||||||
@@ -595,7 +690,7 @@ struct CreatorChannelView: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func communityOverlay(proxy: GeometryProxy) -> some View {
|
private func communityOverlay(proxy: GeometryProxy) -> some View {
|
||||||
if viewModel.selectedTab == .community {
|
if viewModel.selectedTab == .home || viewModel.selectedTab == .community {
|
||||||
if communityViewModel.isShowReportMenu {
|
if communityViewModel.isShowReportMenu {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
CreatorCommunityMenuView(
|
CreatorCommunityMenuView(
|
||||||
@@ -603,7 +698,7 @@ struct CreatorChannelView: View {
|
|||||||
isShowCreatorMenu: isOwnCreatorChannel,
|
isShowCreatorMenu: isOwnCreatorChannel,
|
||||||
isFixed: communityViewModel.selectedPostIsPinned,
|
isFixed: communityViewModel.selectedPostIsPinned,
|
||||||
fixedAction: {
|
fixedAction: {
|
||||||
communityViewModel.updateCommunityPostFixed(creatorId: creatorId)
|
communityViewModel.updateCommunityPostFixed(onSuccess: refreshCommunityFeeds)
|
||||||
},
|
},
|
||||||
modifyAction: showCommunityModify,
|
modifyAction: showCommunityModify,
|
||||||
deleteAction: {
|
deleteAction: {
|
||||||
@@ -638,7 +733,7 @@ struct CreatorChannelView: View {
|
|||||||
button1: SodaV2ActionModalButton(
|
button1: SodaV2ActionModalButton(
|
||||||
label: I18n.Common.delete,
|
label: I18n.Common.delete,
|
||||||
action: {
|
action: {
|
||||||
communityViewModel.deleteCommunityPost(creatorId: creatorId)
|
communityViewModel.deleteCommunityPost(onSuccess: refreshCommunityFeeds)
|
||||||
communityViewModel.isShowDeleteConfirm = false
|
communityViewModel.isShowDeleteConfirm = false
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -650,16 +745,6 @@ struct CreatorChannelView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if communityViewModel.isShowPostPurchaseView {
|
|
||||||
CommunityPostPurchaseDialog(
|
|
||||||
isShowing: $communityViewModel.isShowPostPurchaseView,
|
|
||||||
can: communityViewModel.selectedPostPrice,
|
|
||||||
confirmAction: {
|
|
||||||
communityViewModel.purchaseCommunityPost(creatorId: creatorId)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if communityViewModel.isLoading {
|
if communityViewModel.isLoading {
|
||||||
LoadingView()
|
LoadingView()
|
||||||
}
|
}
|
||||||
@@ -674,7 +759,7 @@ struct CreatorChannelView: View {
|
|||||||
step: .creatorCommunityModify(
|
step: .creatorCommunityModify(
|
||||||
postId: postId,
|
postId: postId,
|
||||||
onSuccess: {
|
onSuccess: {
|
||||||
communityViewModel.fetchFirstPage(creatorId: creatorId)
|
refreshCommunityFeeds()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Combine
|
|||||||
final class CreatorChannelViewModel: ObservableObject {
|
final class CreatorChannelViewModel: ObservableObject {
|
||||||
private let repository = CreatorChannelHomeRepository()
|
private let repository = CreatorChannelHomeRepository()
|
||||||
private let userRepository = UserRepository()
|
private let userRepository = UserRepository()
|
||||||
|
private let reportRepository = ReportRepository()
|
||||||
private let communityRepository = CreatorCommunityRepository()
|
private let communityRepository = CreatorCommunityRepository()
|
||||||
private var subscription = Set<AnyCancellable>()
|
private var subscription = Set<AnyCancellable>()
|
||||||
|
|
||||||
@@ -156,6 +157,75 @@ final class CreatorChannelViewModel: ObservableObject {
|
|||||||
.store(in: &subscription)
|
.store(in: &subscription)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func userBlock(userId: Int, onSuccess: @escaping () -> Void) {
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
userRepository.memberBlock(userId: userId)
|
||||||
|
.sink { [weak self] result in
|
||||||
|
switch result {
|
||||||
|
case .finished:
|
||||||
|
DEBUG_LOG("finish")
|
||||||
|
case .failure(let error):
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
self?.isLoading = false
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] response in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let jsonDecoder = JSONDecoder()
|
||||||
|
let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: response.data)
|
||||||
|
|
||||||
|
if decoded.success {
|
||||||
|
onSuccess()
|
||||||
|
} else {
|
||||||
|
self.errorMessage = decoded.message ?? I18n.Common.commonError
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
self.errorMessage = I18n.Common.commonError
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isLoading = false
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
func report(type: ReportType, userId: Int? = nil, reason: String = I18n.Dialog.MemberProfile.reportProfile) {
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
let request = ReportRequest(type: type, reason: reason, reportedMemberId: userId)
|
||||||
|
reportRepository.report(request: request)
|
||||||
|
.sink { [weak self] result in
|
||||||
|
switch result {
|
||||||
|
case .finished:
|
||||||
|
DEBUG_LOG("finish")
|
||||||
|
case .failure(let error):
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
self?.isLoading = false
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] response in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let jsonDecoder = JSONDecoder()
|
||||||
|
let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: response.data)
|
||||||
|
|
||||||
|
self.errorMessage = decoded.message ?? I18n.Common.commonError
|
||||||
|
self.isShowPopup = true
|
||||||
|
} catch {
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
self.errorMessage = I18n.Common.commonError
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isLoading = false
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
}
|
||||||
|
|
||||||
private func applyApiFailedPlaceholderState() {
|
private func applyApiFailedPlaceholderState() {
|
||||||
response = nil
|
response = nil
|
||||||
isApiFailedPlaceholderVisible = true
|
isApiFailedPlaceholderVisible = true
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ struct CreatorChannelDonationCard: View {
|
|||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
.frame(width: 18, height: 18)
|
.frame(width: 18, height: 18)
|
||||||
|
|
||||||
Text("\(donation.can)캔")
|
Text("\(donation.can)")
|
||||||
.appFont(size: 14, weight: .regular)
|
.appFont(size: 14, weight: .regular)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
@@ -72,7 +72,7 @@ struct CreatorChannelDonationCard: View {
|
|||||||
extension CreatorChannelDonationResponse {
|
extension CreatorChannelDonationResponse {
|
||||||
var messageText: String {
|
var messageText: String {
|
||||||
let trimmedMessage = message.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmedMessage = message.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
return trimmedMessage.isEmpty ? "\(can)캔을 후원하였습니다" : trimmedMessage
|
return trimmedMessage.isEmpty ? I18n.CreatorChannelDonation.defaultMessage(can) : trimmedMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
func relativeTimeText(now: Date = Date()) -> String {
|
func relativeTimeText(now: Date = Date()) -> String {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ struct CreatorChannelDonationCountBar: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: SodaSpacing.s4) {
|
HStack(spacing: SodaSpacing.s4) {
|
||||||
Text("전체")
|
Text(I18n.CreatorChannelCommunity.totalLabel)
|
||||||
.appFont(size: 16, weight: .medium)
|
.appFont(size: 16, weight: .medium)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ struct CreatorChannelDonationEmptyView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: SodaSpacing.s20) {
|
VStack(spacing: SodaSpacing.s20) {
|
||||||
Text("아직 후원이 없습니다.\n처음으로 크리에이터를 후원해 보세요!")
|
Text(I18n.CreatorChannelDonation.emptyMessage)
|
||||||
.appFont(size: 16, weight: .medium)
|
.appFont(size: 16, weight: .medium)
|
||||||
.foregroundColor(Color.gray500)
|
.foregroundColor(Color.gray500)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
@@ -22,7 +22,7 @@ struct CreatorChannelDonationEmptyView: View {
|
|||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
.frame(width: 20, height: 20)
|
.frame(width: 20, height: 20)
|
||||||
|
|
||||||
Text("후원하기")
|
Text(I18n.ContentDetail.DonationDialog.title)
|
||||||
.appFont(size: 16, weight: .medium)
|
.appFont(size: 16, weight: .medium)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ struct CreatorChannelDonationRankingSection: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
if rankings.isEmpty == false {
|
if rankings.isEmpty == false {
|
||||||
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
|
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
|
||||||
Text("후원 랭킹")
|
Text(I18n.Explorer.donationRankingTitle)
|
||||||
.appFont(size: 20, weight: .bold)
|
.appFont(size: 20, weight: .bold)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
@@ -19,7 +19,7 @@ struct CreatorChannelDonationRankingSection: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Button(action: onTapViewAll) {
|
Button(action: onTapViewAll) {
|
||||||
Text("전체보기")
|
Text(I18n.CreatorChannelHome.viewAll)
|
||||||
.appFont(size: 16, weight: .medium)
|
.appFont(size: 16, weight: .medium)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ struct CreatorChannelDonationTabView: View {
|
|||||||
|
|
||||||
private var content: some View {
|
private var content: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
|
if viewModel.donationCount > 0 || viewModel.rankings.isEmpty == false {
|
||||||
CreatorChannelDonationCountBar(donationCount: viewModel.donationCount)
|
CreatorChannelDonationCountBar(donationCount: viewModel.donationCount)
|
||||||
|
}
|
||||||
|
|
||||||
if isEmptyState {
|
if isEmptyState {
|
||||||
CreatorChannelDonationEmptyView(
|
CreatorChannelDonationEmptyView(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ struct CreatorChannelFanTalkCountBar: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: SodaSpacing.s4) {
|
HStack(spacing: SodaSpacing.s4) {
|
||||||
Text("전체")
|
Text(I18n.CreatorChannelCommunity.totalLabel)
|
||||||
.appFont(size: 16, weight: .medium)
|
.appFont(size: 16, weight: .medium)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ struct CreatorChannelFanTalkEmptyView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: SodaSpacing.s20) {
|
VStack(spacing: SodaSpacing.s20) {
|
||||||
Text("아직 응원이 없습니다.\n처음으로 크리에이터를 응원해 보세요!")
|
Text(I18n.CreatorChannelFanTalk.emptyMessage)
|
||||||
.appFont(size: 16, weight: .medium)
|
.appFont(size: 16, weight: .medium)
|
||||||
.foregroundColor(Color.gray500)
|
.foregroundColor(Color.gray500)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
@@ -22,14 +22,14 @@ struct CreatorChannelFanTalkEmptyView: View {
|
|||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
.frame(width: 20, height: 20)
|
.frame(width: 20, height: 20)
|
||||||
|
|
||||||
Text("응원 남기기")
|
Text(I18n.CreatorChannelFanTalk.writeAction)
|
||||||
.appFont(size: 16, weight: .medium)
|
.appFont(size: 16, weight: .medium)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, SodaSpacing.s20)
|
.padding(.horizontal, SodaSpacing.s20)
|
||||||
.frame(height: 44)
|
.frame(height: 44)
|
||||||
.background(Color.soda400)
|
.background(Color.gray800)
|
||||||
.clipShape(Capsule())
|
.clipShape(Capsule())
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ struct CreatorChannelFanTalkListItem: View {
|
|||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
} else {
|
} else {
|
||||||
Button(action: onTapReport) {
|
Button(action: onTapReport) {
|
||||||
Text("신고")
|
Text(I18n.Explorer.report)
|
||||||
.appFont(size: 13, weight: .medium)
|
.appFont(size: 13, weight: .medium)
|
||||||
.foregroundColor(Color.gray500)
|
.foregroundColor(Color.gray500)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ struct CreatorChannelFanTalkTabView: View {
|
|||||||
|
|
||||||
private var content: some View {
|
private var content: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
|
if viewModel.fanTalkCount > 0 {
|
||||||
CreatorChannelFanTalkCountBar(fanTalkCount: viewModel.fanTalkCount)
|
CreatorChannelFanTalkCountBar(fanTalkCount: viewModel.fanTalkCount)
|
||||||
|
}
|
||||||
|
|
||||||
if isEmptyState {
|
if isEmptyState {
|
||||||
CreatorChannelFanTalkEmptyView(
|
CreatorChannelFanTalkEmptyView(
|
||||||
|
|||||||
@@ -25,12 +25,27 @@ struct CreatorChannelFanTalkWriteView: View {
|
|||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
navigationBar
|
navigationBar
|
||||||
|
|
||||||
TextEditor(text: $viewModel.content)
|
TextEditor(text: Binding(
|
||||||
|
get: { viewModel.content },
|
||||||
|
set: { newValue in
|
||||||
|
viewModel.content = String(newValue.prefix(viewModel.maxContentCount))
|
||||||
|
}
|
||||||
|
))
|
||||||
.appFont(size: 16, weight: .regular)
|
.appFont(size: 16, weight: .regular)
|
||||||
.foregroundColor(Color.white)
|
.foregroundColor(viewModel.isContentOverLimit ? Color.red400 : Color.white)
|
||||||
.accentColor(Color.soda400)
|
.accentColor(Color.soda400)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(Color.black)
|
.background(Color.black)
|
||||||
|
.overlay(alignment: .topLeading) {
|
||||||
|
if viewModel.content.isEmpty {
|
||||||
|
Text(I18n.CreatorChannelFanTalk.writePlaceholder)
|
||||||
|
.appFont(size: 16, weight: .regular)
|
||||||
|
.foregroundColor(Color.gray500)
|
||||||
|
.padding(.horizontal, SodaSpacing.s4)
|
||||||
|
.padding(.vertical, SodaSpacing.s8)
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
.focused($isFocused)
|
.focused($isFocused)
|
||||||
.padding(.horizontal, SodaSpacing.s14)
|
.padding(.horizontal, SodaSpacing.s14)
|
||||||
.padding(.top, SodaSpacing.s20)
|
.padding(.top, SodaSpacing.s20)
|
||||||
@@ -56,7 +71,6 @@ struct CreatorChannelFanTalkWriteView: View {
|
|||||||
Text(I18n.Common.cancel)
|
Text(I18n.Common.cancel)
|
||||||
.appFont(size: 16, weight: .regular)
|
.appFont(size: 16, weight: .regular)
|
||||||
.foregroundColor(Color.white)
|
.foregroundColor(Color.white)
|
||||||
.frame(width: 64, height: 56, alignment: .leading)
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
@@ -85,8 +99,8 @@ struct CreatorChannelFanTalkWriteView: View {
|
|||||||
|
|
||||||
Text("\(viewModel.content.count)")
|
Text("\(viewModel.content.count)")
|
||||||
.appFont(size: 16, weight: .medium)
|
.appFont(size: 16, weight: .medium)
|
||||||
.foregroundColor(Color.white)
|
.foregroundColor(viewModel.isContentOverLimit ? Color.red400 : Color.white)
|
||||||
Text("/\(viewModel.maxContentCount)자")
|
Text("/\(viewModel.maxContentCount)")
|
||||||
.appFont(size: 16, weight: .medium)
|
.appFont(size: 16, weight: .medium)
|
||||||
.foregroundColor(Color.gray500)
|
.foregroundColor(Color.gray500)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,15 @@ final class CreatorChannelFanTalkWriteViewModel: ObservableObject {
|
|||||||
|
|
||||||
let maxContentCount = 500
|
let maxContentCount = 500
|
||||||
|
|
||||||
|
var isContentOverLimit: Bool {
|
||||||
|
content.count > maxContentCount
|
||||||
|
}
|
||||||
|
|
||||||
var canSend: Bool {
|
var canSend: Bool {
|
||||||
|
hasContent && isContentOverLimit == false
|
||||||
|
}
|
||||||
|
|
||||||
|
private var hasContent: Bool {
|
||||||
content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
|
content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +48,7 @@ final class CreatorChannelFanTalkWriteViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleCancel() -> Bool {
|
func handleCancel() -> Bool {
|
||||||
guard canSend else { return true }
|
guard hasContent else { return true }
|
||||||
|
|
||||||
shouldShowExitModal = true
|
shouldShowExitModal = true
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -2,25 +2,28 @@ import SwiftUI
|
|||||||
|
|
||||||
struct CreatorChannelCommunitySection: View {
|
struct CreatorChannelCommunitySection: View {
|
||||||
let communities: [CreatorChannelCommunityPostResponse]
|
let communities: [CreatorChannelCommunityPostResponse]
|
||||||
|
let isOwnCreatorChannel: Bool
|
||||||
let onSelectTab: (CreatorChannelTab) -> Void
|
let onSelectTab: (CreatorChannelTab) -> Void
|
||||||
let onTapLike: (Int) -> Void
|
let onTapLike: (Int) -> Void
|
||||||
let onTapComment: (Int) -> Void
|
let onTapMore: (CreatorChannelCommunityPostItem) -> Void
|
||||||
let onTapPurchase: (CreatorChannelCommunityPostResponse) -> Void
|
let onTapDetail: (Int) -> Void
|
||||||
|
|
||||||
private let homeVisibleCount = 3
|
private let homeVisibleCount = 3
|
||||||
|
|
||||||
init(
|
init(
|
||||||
communities: [CreatorChannelCommunityPostResponse],
|
communities: [CreatorChannelCommunityPostResponse],
|
||||||
|
isOwnCreatorChannel: Bool,
|
||||||
onSelectTab: @escaping (CreatorChannelTab) -> Void,
|
onSelectTab: @escaping (CreatorChannelTab) -> Void,
|
||||||
onTapLike: @escaping (Int) -> Void = { _ in },
|
onTapLike: @escaping (Int) -> Void = { _ in },
|
||||||
onTapComment: @escaping (Int) -> Void = { _ in },
|
onTapMore: @escaping (CreatorChannelCommunityPostItem) -> Void = { _ in },
|
||||||
onTapPurchase: @escaping (CreatorChannelCommunityPostResponse) -> Void = { _ in }
|
onTapDetail: @escaping (Int) -> Void = { _ in }
|
||||||
) {
|
) {
|
||||||
self.communities = communities
|
self.communities = communities
|
||||||
|
self.isOwnCreatorChannel = isOwnCreatorChannel
|
||||||
self.onSelectTab = onSelectTab
|
self.onSelectTab = onSelectTab
|
||||||
self.onTapLike = onTapLike
|
self.onTapLike = onTapLike
|
||||||
self.onTapComment = onTapComment
|
self.onTapMore = onTapMore
|
||||||
self.onTapPurchase = onTapPurchase
|
self.onTapDetail = onTapDetail
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -30,27 +33,21 @@ struct CreatorChannelCommunitySection: View {
|
|||||||
|
|
||||||
VStack(alignment: .leading, spacing: SodaSpacing.s8) {
|
VStack(alignment: .leading, spacing: SodaSpacing.s8) {
|
||||||
ForEach(visibleCommunities) { community in
|
ForEach(visibleCommunities) { community in
|
||||||
CommunityPostCard(
|
let post = community.communityPostItem
|
||||||
postId: community.postId,
|
let isOwnPost = post.creatorId == UserDefaults.int(forKey: .userId)
|
||||||
creatorNickname: community.creatorNickname,
|
|
||||||
creatorProfileImageUrl: community.creatorProfileUrl,
|
CreatorChannelCommunityListItem(
|
||||||
content: community.content,
|
post: post,
|
||||||
imageUrl: community.imageUrl,
|
isOwnPost: isOwnPost,
|
||||||
audioUrl: community.audioUrl,
|
isOwnCreatorChannel: isOwnCreatorChannel,
|
||||||
price: community.price,
|
|
||||||
existOrdered: community.existOrdered,
|
|
||||||
createdAt: community.relativeTimeText(),
|
|
||||||
isLike: community.isLiked,
|
|
||||||
likeCount: community.likeCount,
|
|
||||||
commentCount: community.commentCount,
|
|
||||||
onTapLike: {
|
onTapLike: {
|
||||||
onTapLike(community.postId)
|
onTapLike(post.postId)
|
||||||
},
|
},
|
||||||
onTapComment: {
|
onTapMore: {
|
||||||
onTapComment(community.postId)
|
onTapMore(post)
|
||||||
},
|
},
|
||||||
onTapPurchase: {
|
onTapDetail: {
|
||||||
onTapPurchase(community)
|
onTapDetail(post.postId)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -71,6 +68,7 @@ struct CreatorChannelCommunitySection: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
.contentShape(Rectangle())
|
||||||
.padding(.horizontal, SodaSpacing.s14)
|
.padding(.horizontal, SodaSpacing.s14)
|
||||||
}
|
}
|
||||||
.padding(.top, SodaSpacing.s20)
|
.padding(.top, SodaSpacing.s20)
|
||||||
@@ -83,8 +81,24 @@ struct CreatorChannelCommunitySection: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private extension CreatorChannelCommunityPostResponse {
|
private extension CreatorChannelCommunityPostResponse {
|
||||||
func relativeTimeText(now: Date = Date()) -> String {
|
var communityPostItem: CreatorChannelCommunityPostItem {
|
||||||
DateParser.relativeTimeText(fromUTC: dateUtc, fallback: dateUtc, now: now)
|
CreatorChannelCommunityPostItem(
|
||||||
|
postId: postId,
|
||||||
|
creatorId: creatorId,
|
||||||
|
creatorNickname: creatorNickname,
|
||||||
|
creatorProfileUrl: creatorProfileUrl,
|
||||||
|
createdAtUtc: dateUtc,
|
||||||
|
content: content,
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
audioUrl: audioUrl,
|
||||||
|
price: price,
|
||||||
|
isCommentAvailable: isCommentAvailable,
|
||||||
|
likeCount: likeCount,
|
||||||
|
commentCount: commentCount,
|
||||||
|
isPinned: isPinned,
|
||||||
|
existOrdered: existOrdered,
|
||||||
|
isLiked: isLiked
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,10 +121,15 @@ struct CreatorChannelCommunitySection_Previews: PreviewProvider {
|
|||||||
existOrdered: false,
|
existOrdered: false,
|
||||||
likeCount: 5,
|
likeCount: 5,
|
||||||
commentCount: 6,
|
commentCount: 6,
|
||||||
isLiked: false
|
isLiked: false,
|
||||||
|
isCommentAvailable: index != 3,
|
||||||
|
isPinned: index == 1
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onSelectTab: { _ in }
|
isOwnCreatorChannel: true,
|
||||||
|
onSelectTab: { _ in },
|
||||||
|
onTapMore: { _ in },
|
||||||
|
onTapDetail: { _ in }
|
||||||
)
|
)
|
||||||
.background(Color.black)
|
.background(Color.black)
|
||||||
.previewLayout(.sizeThatFits)
|
.previewLayout(.sizeThatFits)
|
||||||
|
|||||||
@@ -78,10 +78,15 @@ private struct CreatorChannelDonationEmptyCard: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
Text("처음으로 크리에이터를\n후원해 보세요!")
|
Text(I18n.CreatorChannelHome.donationEmptyMessage)
|
||||||
.appFont(size: 24, weight: .bold)
|
.appFont(size: 24, weight: .bold)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.lineSpacing(0)
|
.lineSpacing(0)
|
||||||
|
.lineLimit(2)
|
||||||
|
.frame(
|
||||||
|
maxWidth: LanguageHeaderProvider.current == "ja" ? 210 : .infinity,
|
||||||
|
alignment: .leading
|
||||||
|
)
|
||||||
.padding(.top, SodaSpacing.s20)
|
.padding(.top, SodaSpacing.s20)
|
||||||
.padding(.leading, SodaSpacing.s20)
|
.padding(.leading, SodaSpacing.s20)
|
||||||
|
|
||||||
|
|||||||
@@ -2,24 +2,79 @@ import SwiftUI
|
|||||||
|
|
||||||
struct CreatorChannelFanTalkSection: View {
|
struct CreatorChannelFanTalkSection: View {
|
||||||
let fanTalk: CreatorChannelFanTalkSummaryResponse
|
let fanTalk: CreatorChannelFanTalkSummaryResponse
|
||||||
|
let showsWriteButton: Bool
|
||||||
let onSelectTab: (CreatorChannelTab) -> Void
|
let onSelectTab: (CreatorChannelTab) -> Void
|
||||||
|
let onTapWrite: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if let latestFanTalk = fanTalk.latestFanTalk {
|
|
||||||
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
|
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
|
||||||
SectionTitle(title: I18n.CreatorChannelHome.fanTalk) {
|
SectionTitle(title: I18n.CreatorChannelHome.fanTalk) {
|
||||||
onSelectTab(.fanTalk)
|
onSelectTab(.fanTalk)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let latestFanTalk = fanTalk.latestFanTalk {
|
||||||
CreatorChannelFanTalkCard(
|
CreatorChannelFanTalkCard(
|
||||||
totalCount: fanTalk.totalCount,
|
totalCount: fanTalk.totalCount,
|
||||||
latestFanTalk: latestFanTalk
|
latestFanTalk: latestFanTalk
|
||||||
)
|
)
|
||||||
.padding(.horizontal, SodaSpacing.s14)
|
.padding(.horizontal, SodaSpacing.s14)
|
||||||
|
} else {
|
||||||
|
CreatorChannelFanTalkEmptyCard(
|
||||||
|
showsWriteButton: showsWriteButton,
|
||||||
|
onTapWrite: onTapWrite
|
||||||
|
)
|
||||||
|
.padding(.horizontal, SodaSpacing.s14)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(.top, SodaSpacing.s20)
|
.padding(.top, SodaSpacing.s20)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct CreatorChannelFanTalkEmptyCard: View {
|
||||||
|
let showsWriteButton: Bool
|
||||||
|
let onTapWrite: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
Text(I18n.CreatorChannelHome.fanTalkEmptyMessage)
|
||||||
|
.appFont(size: 24, weight: .bold)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.lineSpacing(0)
|
||||||
|
.padding(.top, SodaSpacing.s20)
|
||||||
|
.padding(.leading, SodaSpacing.s20)
|
||||||
|
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
|
||||||
|
if showsWriteButton {
|
||||||
|
Button(action: onTapWrite) {
|
||||||
|
HStack(spacing: SodaSpacing.s6) {
|
||||||
|
Image("ic_plus_no_bg")
|
||||||
|
.resizable()
|
||||||
|
.renderingMode(.template)
|
||||||
|
.foregroundColor(.black)
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
|
||||||
|
Text(I18n.CreatorChannelFanTalk.writeAction)
|
||||||
|
.appFont(size: 16, weight: .medium)
|
||||||
|
.foregroundColor(.black)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(SodaSpacing.s12)
|
||||||
|
.background(Color.white)
|
||||||
|
.clipShape(Capsule())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.padding(.horizontal, SodaSpacing.s20)
|
||||||
|
.padding(.bottom, SodaSpacing.s20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: 196)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.background(Color.gray900)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct CreatorChannelFanTalkCard: View {
|
private struct CreatorChannelFanTalkCard: View {
|
||||||
@@ -77,7 +132,9 @@ struct CreatorChannelFanTalkSection_Previews: PreviewProvider {
|
|||||||
createdAtUtc: "2026-07-03T03:44:00Z"
|
createdAtUtc: "2026-07-03T03:44:00Z"
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
onSelectTab: { _ in }
|
showsWriteButton: true,
|
||||||
|
onSelectTab: { _ in },
|
||||||
|
onTapWrite: {}
|
||||||
)
|
)
|
||||||
.background(Color.black)
|
.background(Color.black)
|
||||||
.previewLayout(.sizeThatFits)
|
.previewLayout(.sizeThatFits)
|
||||||
|
|||||||
@@ -2,20 +2,14 @@ import SwiftUI
|
|||||||
|
|
||||||
struct CreatorChannelNoticeSection: View {
|
struct CreatorChannelNoticeSection: View {
|
||||||
let notices: [CreatorChannelCommunityPostResponse]
|
let notices: [CreatorChannelCommunityPostResponse]
|
||||||
let onTapNotice: (Int) -> Void
|
let onTapDetail: (Int) -> Void
|
||||||
let onTapComment: (Int) -> Void
|
|
||||||
let onTapPurchase: (CreatorChannelCommunityPostResponse) -> Void
|
|
||||||
|
|
||||||
init(
|
init(
|
||||||
notices: [CreatorChannelCommunityPostResponse],
|
notices: [CreatorChannelCommunityPostResponse],
|
||||||
onTapNotice: @escaping (Int) -> Void = { _ in },
|
onTapDetail: @escaping (Int) -> Void = { _ in }
|
||||||
onTapComment: @escaping (Int) -> Void = { _ in },
|
|
||||||
onTapPurchase: @escaping (CreatorChannelCommunityPostResponse) -> Void = { _ in }
|
|
||||||
) {
|
) {
|
||||||
self.notices = notices
|
self.notices = notices
|
||||||
self.onTapNotice = onTapNotice
|
self.onTapDetail = onTapDetail
|
||||||
self.onTapComment = onTapComment
|
|
||||||
self.onTapPurchase = onTapPurchase
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -28,14 +22,8 @@ struct CreatorChannelNoticeSection: View {
|
|||||||
ForEach(notices) { notice in
|
ForEach(notices) { notice in
|
||||||
CreatorChannelNoticeCard(
|
CreatorChannelNoticeCard(
|
||||||
notice: notice,
|
notice: notice,
|
||||||
onTapNotice: {
|
onTapDetail: {
|
||||||
onTapNotice(notice.postId)
|
onTapDetail(notice.postId)
|
||||||
},
|
|
||||||
onTapComment: {
|
|
||||||
onTapComment(notice.postId)
|
|
||||||
},
|
|
||||||
onTapPurchase: {
|
|
||||||
onTapPurchase(notice)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.frame(width: 374)
|
.frame(width: 374)
|
||||||
@@ -51,9 +39,7 @@ struct CreatorChannelNoticeSection: View {
|
|||||||
|
|
||||||
private struct CreatorChannelNoticeCard: View {
|
private struct CreatorChannelNoticeCard: View {
|
||||||
let notice: CreatorChannelCommunityPostResponse
|
let notice: CreatorChannelCommunityPostResponse
|
||||||
let onTapNotice: () -> Void
|
let onTapDetail: () -> Void
|
||||||
let onTapComment: () -> Void
|
|
||||||
let onTapPurchase: () -> Void
|
|
||||||
|
|
||||||
@StateObject private var playManager = CreatorCommunityMediaPlayerManager.shared
|
@StateObject private var playManager = CreatorCommunityMediaPlayerManager.shared
|
||||||
@StateObject private var contentPlayManager = ContentPlayManager.shared
|
@StateObject private var contentPlayManager = ContentPlayManager.shared
|
||||||
@@ -69,7 +55,7 @@ private struct CreatorChannelNoticeCard: View {
|
|||||||
.background(Color.gray900)
|
.background(Color.gray900)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture(perform: onTapNotice)
|
.onTapGesture(perform: onTapDetail)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var noticeLabel: some View {
|
private var noticeLabel: some View {
|
||||||
@@ -164,7 +150,6 @@ private struct CreatorChannelNoticeCard: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var paidLockOverlay: some View {
|
private var paidLockOverlay: some View {
|
||||||
Button(action: onTapPurchase) {
|
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.gray700.opacity(0.35)
|
Color.gray700.opacity(0.35)
|
||||||
|
|
||||||
@@ -189,15 +174,13 @@ private struct CreatorChannelNoticeCard: View {
|
|||||||
.clipShape(Capsule())
|
.clipShape(Capsule())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.frame(width: 92, height: 92)
|
.frame(width: 92, height: 92)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||||
}
|
}
|
||||||
|
|
||||||
private var reactionBar: some View {
|
private var reactionBar: some View {
|
||||||
HStack(spacing: 15) {
|
HStack(spacing: 15) {
|
||||||
Button(action: onTapComment) {
|
Button(action: onTapDetail) {
|
||||||
HStack(spacing: SodaSpacing.s4) {
|
HStack(spacing: SodaSpacing.s4) {
|
||||||
Image("ic_feed_community_reply")
|
Image("ic_feed_community_reply")
|
||||||
.resizable()
|
.resizable()
|
||||||
@@ -264,7 +247,9 @@ struct CreatorChannelNoticeSection_Previews: PreviewProvider {
|
|||||||
existOrdered: true,
|
existOrdered: true,
|
||||||
likeCount: 5,
|
likeCount: 5,
|
||||||
commentCount: 2,
|
commentCount: 2,
|
||||||
isLiked: false
|
isLiked: false,
|
||||||
|
isCommentAvailable: true,
|
||||||
|
isPinned: true
|
||||||
),
|
),
|
||||||
CreatorChannelCommunityPostResponse(
|
CreatorChannelCommunityPostResponse(
|
||||||
postId: 2,
|
postId: 2,
|
||||||
@@ -279,7 +264,9 @@ struct CreatorChannelNoticeSection_Previews: PreviewProvider {
|
|||||||
existOrdered: false,
|
existOrdered: false,
|
||||||
likeCount: 3,
|
likeCount: 3,
|
||||||
commentCount: 1,
|
commentCount: 1,
|
||||||
isLiked: false
|
isLiked: false,
|
||||||
|
isCommentAvailable: true,
|
||||||
|
isPinned: false
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,32 +2,44 @@ import SwiftUI
|
|||||||
|
|
||||||
struct CreatorChannelHomeView: View {
|
struct CreatorChannelHomeView: View {
|
||||||
let response: CreatorChannelHomeResponse
|
let response: CreatorChannelHomeResponse
|
||||||
|
let isOwnCreatorChannel: Bool
|
||||||
let onSelectTab: (CreatorChannelTab) -> Void
|
let onSelectTab: (CreatorChannelTab) -> Void
|
||||||
let onTapLive: (Int) -> Void
|
let onTapLive: (Int) -> Void
|
||||||
let onTapContent: (Int) -> Void
|
let onTapContent: (Int) -> Void
|
||||||
let onTapDonate: () -> Void
|
let onTapDonate: () -> Void
|
||||||
|
let onTapFanTalkWrite: () -> Void
|
||||||
let onTapSchedule: (CreatorActivityType, Int) -> Void
|
let onTapSchedule: (CreatorActivityType, Int) -> Void
|
||||||
let onTapSeries: (Int) -> Void
|
let onTapSeries: (Int) -> Void
|
||||||
let onTapCommunityLike: (Int) -> Void
|
let onTapCommunityLike: (Int) -> Void
|
||||||
|
let onTapCommunityMore: (CreatorChannelCommunityPostItem) -> Void
|
||||||
|
let onTapCommunityDetail: (Int) -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
response: CreatorChannelHomeResponse,
|
response: CreatorChannelHomeResponse,
|
||||||
|
isOwnCreatorChannel: Bool,
|
||||||
onSelectTab: @escaping (CreatorChannelTab) -> Void,
|
onSelectTab: @escaping (CreatorChannelTab) -> Void,
|
||||||
onTapLive: @escaping (Int) -> Void = { _ in },
|
onTapLive: @escaping (Int) -> Void = { _ in },
|
||||||
onTapContent: @escaping (Int) -> Void = { _ in },
|
onTapContent: @escaping (Int) -> Void = { _ in },
|
||||||
onTapDonate: @escaping () -> Void = {},
|
onTapDonate: @escaping () -> Void = {},
|
||||||
|
onTapFanTalkWrite: @escaping () -> Void = {},
|
||||||
onTapSchedule: @escaping (CreatorActivityType, Int) -> Void = { _, _ in },
|
onTapSchedule: @escaping (CreatorActivityType, Int) -> Void = { _, _ in },
|
||||||
onTapSeries: @escaping (Int) -> Void = { _ in },
|
onTapSeries: @escaping (Int) -> Void = { _ in },
|
||||||
onTapCommunityLike: @escaping (Int) -> Void = { _ in }
|
onTapCommunityLike: @escaping (Int) -> Void = { _ in },
|
||||||
|
onTapCommunityMore: @escaping (CreatorChannelCommunityPostItem) -> Void = { _ in },
|
||||||
|
onTapCommunityDetail: @escaping (Int) -> Void = { _ in }
|
||||||
) {
|
) {
|
||||||
self.response = response
|
self.response = response
|
||||||
|
self.isOwnCreatorChannel = isOwnCreatorChannel
|
||||||
self.onSelectTab = onSelectTab
|
self.onSelectTab = onSelectTab
|
||||||
self.onTapLive = onTapLive
|
self.onTapLive = onTapLive
|
||||||
self.onTapContent = onTapContent
|
self.onTapContent = onTapContent
|
||||||
self.onTapDonate = onTapDonate
|
self.onTapDonate = onTapDonate
|
||||||
|
self.onTapFanTalkWrite = onTapFanTalkWrite
|
||||||
self.onTapSchedule = onTapSchedule
|
self.onTapSchedule = onTapSchedule
|
||||||
self.onTapSeries = onTapSeries
|
self.onTapSeries = onTapSeries
|
||||||
self.onTapCommunityLike = onTapCommunityLike
|
self.onTapCommunityLike = onTapCommunityLike
|
||||||
|
self.onTapCommunityMore = onTapCommunityMore
|
||||||
|
self.onTapCommunityDetail = onTapCommunityDetail
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -48,7 +60,10 @@ struct CreatorChannelHomeView: View {
|
|||||||
onTapDonate: onTapDonate
|
onTapDonate: onTapDonate
|
||||||
)
|
)
|
||||||
|
|
||||||
CreatorChannelNoticeSection(notices: response.notices)
|
CreatorChannelNoticeSection(
|
||||||
|
notices: response.notices,
|
||||||
|
onTapDetail: onTapCommunityDetail
|
||||||
|
)
|
||||||
|
|
||||||
CreatorChannelScheduleSection(
|
CreatorChannelScheduleSection(
|
||||||
schedules: response.schedules,
|
schedules: response.schedules,
|
||||||
@@ -69,13 +84,18 @@ struct CreatorChannelHomeView: View {
|
|||||||
|
|
||||||
CreatorChannelCommunitySection(
|
CreatorChannelCommunitySection(
|
||||||
communities: response.communities,
|
communities: response.communities,
|
||||||
|
isOwnCreatorChannel: isOwnCreatorChannel,
|
||||||
onSelectTab: onSelectTab,
|
onSelectTab: onSelectTab,
|
||||||
onTapLike: onTapCommunityLike
|
onTapLike: onTapCommunityLike,
|
||||||
|
onTapMore: onTapCommunityMore,
|
||||||
|
onTapDetail: onTapCommunityDetail
|
||||||
)
|
)
|
||||||
|
|
||||||
CreatorChannelFanTalkSection(
|
CreatorChannelFanTalkSection(
|
||||||
fanTalk: response.fanTalk,
|
fanTalk: response.fanTalk,
|
||||||
onSelectTab: onSelectTab
|
showsWriteButton: isOwnCreatorChannel == false,
|
||||||
|
onSelectTab: onSelectTab,
|
||||||
|
onTapWrite: onTapFanTalkWrite
|
||||||
)
|
)
|
||||||
|
|
||||||
CreatorChannelIntroduceSection(introduce: response.introduce)
|
CreatorChannelIntroduceSection(introduce: response.introduce)
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ struct CreatorChannelCommunityPostResponse: Decodable, Identifiable {
|
|||||||
let likeCount: Int
|
let likeCount: Int
|
||||||
let commentCount: Int
|
let commentCount: Int
|
||||||
let isLiked: Bool
|
let isLiked: Bool
|
||||||
|
let isCommentAvailable: Bool
|
||||||
|
let isPinned: Bool
|
||||||
|
|
||||||
var id: Int { postId }
|
var id: Int { postId }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,19 +12,19 @@ enum CreatorChannelTab: CaseIterable, Hashable {
|
|||||||
var title: String {
|
var title: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .home:
|
case .home:
|
||||||
return "홈"
|
return I18n.CreatorChannelHome.home
|
||||||
case .live:
|
case .live:
|
||||||
return "라이브"
|
return I18n.CreatorChannelHome.live
|
||||||
case .audio:
|
case .audio:
|
||||||
return "오디오"
|
return I18n.CreatorChannelHome.audio
|
||||||
case .series:
|
case .series:
|
||||||
return "시리즈"
|
return I18n.CreatorChannelHome.series
|
||||||
case .community:
|
case .community:
|
||||||
return "커뮤니티"
|
return I18n.CreatorChannelHome.community
|
||||||
case .fanTalk:
|
case .fanTalk:
|
||||||
return "팬Talk"
|
return I18n.CreatorChannelHome.fanTalk
|
||||||
case .donation:
|
case .donation:
|
||||||
return "후원"
|
return I18n.CreatorChannelHome.donation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,18 +54,14 @@ struct MainChatView: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var chatListContent: some View {
|
private var chatListContent: some View {
|
||||||
if viewModel.isLoading && viewModel.rooms.isEmpty {
|
if viewModel.isLoading && !viewModel.hasLoaded {
|
||||||
ProgressView()
|
loadingContent
|
||||||
.progressViewStyle(.circular)
|
|
||||||
.tint(Color.soda400)
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
} else if viewModel.shouldShowEmptyState {
|
} else if viewModel.shouldShowEmptyState {
|
||||||
Text(I18n.MainChat.emptyStateMessage)
|
emptyContent
|
||||||
.appFont(size: 16, weight: .medium)
|
|
||||||
.foregroundColor(Color.gray500)
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
} else {
|
} else {
|
||||||
ScrollView(showsIndicators: false) {
|
LottieRefreshableScrollView(action: {
|
||||||
|
await viewModel.refresh()
|
||||||
|
}) {
|
||||||
LazyVStack(spacing: 0) {
|
LazyVStack(spacing: 0) {
|
||||||
ForEach(viewModel.rooms) { room in
|
ForEach(viewModel.rooms) { room in
|
||||||
MainChatRoomListItem(room: room) { roomId, chatType in
|
MainChatRoomListItem(room: room) { roomId, chatType in
|
||||||
@@ -88,6 +84,34 @@ struct MainChatView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var loadingContent: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
LottieRefreshableScrollView(action: {
|
||||||
|
await viewModel.refresh()
|
||||||
|
}) {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(.circular)
|
||||||
|
.tint(Color.soda400)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(minHeight: geometry.size.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var emptyContent: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
LottieRefreshableScrollView(action: {
|
||||||
|
await viewModel.refresh()
|
||||||
|
}) {
|
||||||
|
Text(I18n.MainChat.emptyStateMessage)
|
||||||
|
.appFont(size: 16, weight: .medium)
|
||||||
|
.foregroundColor(Color.gray500)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(minHeight: geometry.size.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func handleChatRoomTap(roomId: Int, chatType: String) {
|
private func handleChatRoomTap(roomId: Int, chatType: String) {
|
||||||
guard roomId > 0 else { return }
|
guard roomId > 0 else { return }
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ final class MainChatViewModel: ObservableObject {
|
|||||||
fetchChatRooms(filter: filter, cursor: nil, isNextPage: false)
|
fetchChatRooms(filter: filter, cursor: nil, isNextPage: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func refresh() async {
|
||||||
|
if !isLoading {
|
||||||
|
fetchChatRooms(filter: selectedFilter, cursor: nil, isNextPage: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
for await isLoading in $isLoading.values {
|
||||||
|
if !isLoading { return }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func applyFilter(_ filter: MainChatFilter) {
|
func applyFilter(_ filter: MainChatFilter) {
|
||||||
guard selectedFilter != filter else { return }
|
guard selectedFilter != filter else { return }
|
||||||
fetchFirstPage(filter: filter)
|
fetchFirstPage(filter: filter)
|
||||||
@@ -42,7 +52,11 @@ final class MainChatViewModel: ObservableObject {
|
|||||||
fetchChatRooms(filter: selectedFilter, cursor: nextCursor, isNextPage: true)
|
fetchChatRooms(filter: selectedFilter, cursor: nextCursor, isNextPage: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fetchChatRooms(filter: MainChatFilter, cursor: String?, isNextPage: Bool) {
|
private func fetchChatRooms(
|
||||||
|
filter: MainChatFilter,
|
||||||
|
cursor: String?,
|
||||||
|
isNextPage: Bool
|
||||||
|
) {
|
||||||
latestRequestId += 1
|
latestRequestId += 1
|
||||||
let requestId = latestRequestId
|
let requestId = latestRequestId
|
||||||
|
|
||||||
|
|||||||
@@ -70,13 +70,13 @@ struct MainContentAllView: View {
|
|||||||
sortBar
|
sortBar
|
||||||
|
|
||||||
if viewModel.isLoading && viewModel.hasLoaded == false {
|
if viewModel.isLoading && viewModel.hasLoaded == false {
|
||||||
ProgressView()
|
loadingContent
|
||||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
} else if viewModel.shouldShowEmptyState {
|
} else if viewModel.shouldShowEmptyState {
|
||||||
MainContentAudioEmptyStateView(message: I18n.MainContentAll.emptyStateMessage)
|
emptyContent
|
||||||
} else {
|
} else {
|
||||||
ScrollView(showsIndicators: false) {
|
LottieRefreshableScrollView(action: {
|
||||||
|
await viewModel.refresh()
|
||||||
|
}) {
|
||||||
gridContent
|
gridContent
|
||||||
.padding(.horizontal, SodaSpacing.s14)
|
.padding(.horizontal, SodaSpacing.s14)
|
||||||
.padding(.top, SodaSpacing.s8)
|
.padding(.top, SodaSpacing.s8)
|
||||||
@@ -92,6 +92,30 @@ struct MainContentAllView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var loadingContent: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
LottieRefreshableScrollView(action: {
|
||||||
|
await viewModel.refresh()
|
||||||
|
}) {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(minHeight: geometry.size.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var emptyContent: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
LottieRefreshableScrollView(action: {
|
||||||
|
await viewModel.refresh()
|
||||||
|
}) {
|
||||||
|
MainContentAudioEmptyStateView(message: I18n.MainContentAll.emptyStateMessage)
|
||||||
|
.frame(minHeight: geometry.size.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var typeFilter: some View {
|
private var typeFilter: some View {
|
||||||
CapsuleTabBar(
|
CapsuleTabBar(
|
||||||
items: MainContentAllType.allCases,
|
items: MainContentAllType.allCases,
|
||||||
@@ -197,4 +221,3 @@ struct MainContentAllView_Previews: PreviewProvider {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ final class MainContentAllViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fetchFirstPageIfNeeded() {
|
func fetchFirstPageIfNeeded() {
|
||||||
guard hasLoaded == false else { return }
|
guard hasLoaded == false, isLoading == false else { return }
|
||||||
fetchFirstPage()
|
fetchFirstPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +40,16 @@ final class MainContentAllViewModel: ObservableObject {
|
|||||||
fetchContents(page: 0, isNextPage: false)
|
fetchContents(page: 0, isNextPage: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func refresh() async {
|
||||||
|
if !isLoading {
|
||||||
|
fetchFirstPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
for await isLoading in $isLoading.values {
|
||||||
|
if !isLoading { return }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func apply(type: MainContentAllType, sort: ContentSort) {
|
func apply(type: MainContentAllType, sort: ContentSort) {
|
||||||
guard allowedSorts(for: type).contains(sort) else { return }
|
guard allowedSorts(for: type).contains(sort) else { return }
|
||||||
|
|
||||||
@@ -247,4 +257,3 @@ extension SeriesPublishedDaysOfWeek {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ struct MainContentView: View {
|
|||||||
onTapSeries: onTapSeries,
|
onTapSeries: onTapSeries,
|
||||||
onTapLatestAudioAll: { applyAllTab(type: .audio, sort: .latest) },
|
onTapLatestAudioAll: { applyAllTab(type: .audio, sort: .latest) },
|
||||||
onTapNewAndHotAll: onTapNewAndHotAll,
|
onTapNewAndHotAll: onTapNewAndHotAll,
|
||||||
onTapVoiceOnOnlyAll: { applyAllTab(type: .original, sort: .latest) },
|
onTapSodaLiveOnlyAll: { applyAllTab(type: .original, sort: .latest) },
|
||||||
onTapFreeAudioAll: { applyAllTab(type: .free, sort: .popular) },
|
onTapFreeAudioAll: { applyAllTab(type: .free, sort: .popular) },
|
||||||
onTapPointAudioAll: { applyAllTab(type: .point, sort: .popular) }
|
onTapPointAudioAll: { applyAllTab(type: .point, sort: .popular) }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,12 +22,9 @@ struct MainContentRankingView: View {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if viewModel.isLoading && !viewModel.hasLoaded {
|
if viewModel.isLoading && !viewModel.hasLoaded {
|
||||||
ProgressView()
|
loadingContent
|
||||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
} else if viewModel.shouldShowEmptyState {
|
} else if viewModel.shouldShowEmptyState {
|
||||||
MainContentAudioRankingEmptyStateView(message: viewModel.emptyStateMessage)
|
emptyContent
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
} else {
|
} else {
|
||||||
rankingContent
|
rankingContent
|
||||||
}
|
}
|
||||||
@@ -41,8 +38,23 @@ struct MainContentRankingView: View {
|
|||||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var loadingContent: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
LottieRefreshableScrollView(action: {
|
||||||
|
await viewModel.refresh()
|
||||||
|
}) {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(minHeight: geometry.size.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var rankingContent: some View {
|
private var rankingContent: some View {
|
||||||
ScrollView(showsIndicators: false) {
|
LottieRefreshableScrollView(action: {
|
||||||
|
await viewModel.refresh()
|
||||||
|
}) {
|
||||||
VStack(alignment: .leading, spacing: SodaSpacing.s8) {
|
VStack(alignment: .leading, spacing: SodaSpacing.s8) {
|
||||||
if let topItem {
|
if let topItem {
|
||||||
MainContentAudioRankingTopCard(
|
MainContentAudioRankingTopCard(
|
||||||
@@ -95,6 +107,17 @@ struct MainContentRankingView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var emptyContent: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
LottieRefreshableScrollView(action: {
|
||||||
|
await viewModel.refresh()
|
||||||
|
}) {
|
||||||
|
MainContentAudioRankingEmptyStateView(message: viewModel.emptyStateMessage)
|
||||||
|
.frame(minHeight: geometry.size.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var twoColumns: [GridItem] {
|
private var twoColumns: [GridItem] {
|
||||||
[GridItem(.flexible()), GridItem(.flexible())]
|
[GridItem(.flexible()), GridItem(.flexible())]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,19 +32,17 @@ final class MainContentRankingViewModel: ObservableObject {
|
|||||||
|
|
||||||
repository.getAudioRankings(type: selectedType)
|
repository.getAudioRankings(type: selectedType)
|
||||||
.sink { [weak self] result in
|
.sink { [weak self] result in
|
||||||
guard let self else { return }
|
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .finished:
|
case .finished:
|
||||||
DEBUG_LOG("finish")
|
DEBUG_LOG("finish")
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
ERROR_LOG(error.localizedDescription)
|
ERROR_LOG(error.localizedDescription)
|
||||||
self.items = []
|
self?.items = []
|
||||||
self.showRankChange = false
|
self?.showRankChange = false
|
||||||
self.errorMessage = I18n.MainContentRanking.loadFailedMessage
|
self?.errorMessage = I18n.MainContentRanking.loadFailedMessage
|
||||||
self.isShowPopup = true
|
self?.isShowPopup = true
|
||||||
self.isLoading = false
|
self?.isLoading = false
|
||||||
self.hasLoaded = true
|
self?.hasLoaded = true
|
||||||
}
|
}
|
||||||
} receiveValue: { [weak self] response in
|
} receiveValue: { [weak self] response in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
@@ -79,6 +77,16 @@ final class MainContentRankingViewModel: ObservableObject {
|
|||||||
.store(in: &subscription)
|
.store(in: &subscription)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func refresh() async {
|
||||||
|
if !isLoading {
|
||||||
|
fetchRankings()
|
||||||
|
}
|
||||||
|
|
||||||
|
for await isLoading in $isLoading.values {
|
||||||
|
if !isLoading { return }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func selectType(_ type: AudioRankingType) {
|
func selectType(_ type: AudioRankingType) {
|
||||||
guard selectedType != type else { return }
|
guard selectedType != type else { return }
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ struct MainContentRecommendationView: View {
|
|||||||
let onTapSeries: (Int) -> Void
|
let onTapSeries: (Int) -> Void
|
||||||
let onTapLatestAudioAll: () -> Void
|
let onTapLatestAudioAll: () -> Void
|
||||||
let onTapNewAndHotAll: () -> Void
|
let onTapNewAndHotAll: () -> Void
|
||||||
let onTapVoiceOnOnlyAll: () -> Void
|
let onTapSodaLiveOnlyAll: () -> Void
|
||||||
let onTapFreeAudioAll: () -> Void
|
let onTapFreeAudioAll: () -> Void
|
||||||
let onTapPointAudioAll: () -> Void
|
let onTapPointAudioAll: () -> Void
|
||||||
|
|
||||||
@@ -17,13 +17,14 @@ struct MainContentRecommendationView: View {
|
|||||||
Color.black
|
Color.black
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
if viewModel.isLoading && viewModel.recommendations == nil {
|
if viewModel.isLoading && !viewModel.hasLoaded {
|
||||||
ProgressView()
|
loadingContent
|
||||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
|
||||||
} else if viewModel.shouldShowEmptyState {
|
} else if viewModel.shouldShowEmptyState {
|
||||||
MainContentAudioEmptyStateView(message: viewModel.emptyStateMessage)
|
emptyContent
|
||||||
} else {
|
} else {
|
||||||
ScrollView(showsIndicators: false) {
|
LottieRefreshableScrollView(action: {
|
||||||
|
await viewModel.refresh()
|
||||||
|
}) {
|
||||||
VStack(alignment: .leading, spacing: SodaSpacing.s24) {
|
VStack(alignment: .leading, spacing: SodaSpacing.s24) {
|
||||||
if let recommendations = viewModel.recommendations {
|
if let recommendations = viewModel.recommendations {
|
||||||
MainContentAudioBannerSection(
|
MainContentAudioBannerSection(
|
||||||
@@ -32,9 +33,9 @@ struct MainContentRecommendationView: View {
|
|||||||
)
|
)
|
||||||
|
|
||||||
MainContentVoiceOnOnlySection(
|
MainContentVoiceOnOnlySection(
|
||||||
title: I18n.MainContentRecommendation.voiceOnOnlySectionTitle,
|
title: I18n.MainContentRecommendation.sodaLiveOnlySectionTitle,
|
||||||
items: recommendations.originalSeries,
|
items: recommendations.originalSeries,
|
||||||
action: onTapVoiceOnOnlyAll,
|
action: onTapSodaLiveOnlyAll,
|
||||||
onTapSeries: onTapSeries
|
onTapSeries: onTapSeries
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -94,4 +95,28 @@ struct MainContentRecommendationView: View {
|
|||||||
autohideIn: 2
|
autohideIn: 2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var loadingContent: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
LottieRefreshableScrollView(action: {
|
||||||
|
await viewModel.refresh()
|
||||||
|
}) {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(minHeight: geometry.size.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var emptyContent: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
LottieRefreshableScrollView(action: {
|
||||||
|
await viewModel.refresh()
|
||||||
|
}) {
|
||||||
|
MainContentAudioEmptyStateView(message: viewModel.emptyStateMessage)
|
||||||
|
.frame(minHeight: geometry.size.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,17 +30,15 @@ final class MainContentRecommendationViewModel: ObservableObject {
|
|||||||
|
|
||||||
repository.getAudioRecommendations()
|
repository.getAudioRecommendations()
|
||||||
.sink { [weak self] result in
|
.sink { [weak self] result in
|
||||||
guard let self else { return }
|
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .finished:
|
case .finished:
|
||||||
DEBUG_LOG("finish")
|
DEBUG_LOG("finish")
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
ERROR_LOG(error.localizedDescription)
|
ERROR_LOG(error.localizedDescription)
|
||||||
self.errorMessage = I18n.MainContentRecommendation.loadFailedMessage
|
self?.errorMessage = I18n.MainContentRecommendation.loadFailedMessage
|
||||||
self.isShowPopup = true
|
self?.isShowPopup = true
|
||||||
self.isLoading = false
|
self?.isLoading = false
|
||||||
self.hasLoaded = true
|
self?.hasLoaded = true
|
||||||
}
|
}
|
||||||
} receiveValue: { [weak self] response in
|
} receiveValue: { [weak self] response in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
@@ -69,4 +67,14 @@ final class MainContentRecommendationViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
.store(in: &subscription)
|
.store(in: &subscription)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func refresh() async {
|
||||||
|
if !isLoading {
|
||||||
|
fetchRecommendations()
|
||||||
|
}
|
||||||
|
|
||||||
|
for await isLoading in $isLoading.values {
|
||||||
|
if !isLoading { return }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ struct MainHomeFollowingCreatorSection: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
HStack(alignment: .top, spacing: SodaSpacing.s12) {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
LazyHStack(alignment: .top, spacing: SodaSpacing.s12) {
|
LazyHStack(alignment: .top, spacing: SodaSpacing.s12) {
|
||||||
ForEach(followingCreators) { creator in
|
ForEach(followingCreators) { creator in
|
||||||
@@ -29,23 +30,22 @@ struct MainHomeFollowingCreatorSection: View {
|
|||||||
}
|
}
|
||||||
.frame(width: 72)
|
.frame(width: 72)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.padding(.leading, SodaSpacing.s20)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
Button(action: onTapAll) {
|
Button(action: onTapAll) {
|
||||||
Text(I18n.HomeFollowing.allButtonTitle)
|
Text(I18n.HomeFollowing.allButtonTitle)
|
||||||
.appFont(.body4)
|
.appFont(.body5)
|
||||||
.foregroundColor(Color.soda400)
|
.foregroundColor(Color.soda400)
|
||||||
.padding(.horizontal, 16)
|
.frame(width: 70, height: 102, alignment: .center)
|
||||||
.frame(height: allButtonHeight)
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, SodaSpacing.s20)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var allButtonHeight: CGFloat { 72 + SodaSpacing.s8 + 20 }
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MainHomeFollowingCreatorSection_Previews: PreviewProvider {
|
struct MainHomeFollowingCreatorSection_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack(spacing: SodaSpacing.s20) {
|
VStack(spacing: SodaSpacing.s20) {
|
||||||
|
|||||||
@@ -46,20 +46,19 @@ struct MainHomeFollowingView: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var content: some View {
|
private var content: some View {
|
||||||
if viewModel.isLoading {
|
if viewModel.isLoading && !viewModel.hasLoaded {
|
||||||
ProgressView()
|
loadingContent
|
||||||
.tint(.white)
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
.background(Color.black)
|
|
||||||
} else if viewModel.isLoginRequired {
|
} else if viewModel.isLoginRequired {
|
||||||
MainHomeFollowingEmptyStateView(
|
emptyContent(
|
||||||
message: I18n.HomeFollowing.loginRequiredMessage,
|
message: I18n.HomeFollowing.loginRequiredMessage,
|
||||||
loginAction: onTapLogin
|
loginAction: onTapLogin
|
||||||
)
|
)
|
||||||
} else if !viewModel.message.isEmpty {
|
} else if !viewModel.message.isEmpty {
|
||||||
MainHomeFollowingEmptyStateView(message: viewModel.message)
|
emptyContent(message: viewModel.message)
|
||||||
} else if let response = viewModel.response {
|
} else if let response = viewModel.response {
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
LottieRefreshableScrollView(action: {
|
||||||
|
await viewModel.refresh()
|
||||||
|
}) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
if !response.followingCreators.isEmpty {
|
if !response.followingCreators.isEmpty {
|
||||||
MainHomeFollowingCreatorSection(
|
MainHomeFollowingCreatorSection(
|
||||||
@@ -108,7 +107,35 @@ struct MainHomeFollowingView: View {
|
|||||||
}
|
}
|
||||||
.background(Color.black)
|
.background(Color.black)
|
||||||
} else {
|
} else {
|
||||||
MainHomeFollowingEmptyStateView(message: I18n.HomeFollowing.emptyStateMessage)
|
emptyContent(message: I18n.HomeFollowing.emptyStateMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var loadingContent: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
LottieRefreshableScrollView(action: {
|
||||||
|
await viewModel.refresh()
|
||||||
|
}) {
|
||||||
|
ProgressView()
|
||||||
|
.tint(.white)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(minHeight: geometry.size.height)
|
||||||
|
}
|
||||||
|
.background(Color.black)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func emptyContent(message: String, loginAction: (() -> Void)? = nil) -> some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
LottieRefreshableScrollView(action: {
|
||||||
|
await viewModel.refresh()
|
||||||
|
}) {
|
||||||
|
MainHomeFollowingEmptyStateView(
|
||||||
|
message: message,
|
||||||
|
loginAction: loginAction
|
||||||
|
)
|
||||||
|
.frame(minHeight: geometry.size.height)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,18 +17,16 @@ final class MainHomeFollowingViewModel: ObservableObject {
|
|||||||
|
|
||||||
repository.getFollowing()
|
repository.getFollowing()
|
||||||
.sink { [weak self] result in
|
.sink { [weak self] result in
|
||||||
guard let self else { return }
|
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .finished:
|
case .finished:
|
||||||
DEBUG_LOG("finish")
|
DEBUG_LOG("finish")
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
ERROR_LOG(error.localizedDescription)
|
ERROR_LOG(error.localizedDescription)
|
||||||
self.response = nil
|
self?.response = nil
|
||||||
self.isLoginRequired = false
|
self?.isLoginRequired = false
|
||||||
self.message = I18n.HomeFollowing.loadFailedMessage
|
self?.message = I18n.HomeFollowing.loadFailedMessage
|
||||||
self.hasLoaded = true
|
self?.hasLoaded = true
|
||||||
self.isLoading = false
|
self?.isLoading = false
|
||||||
}
|
}
|
||||||
} receiveValue: { [weak self] response in
|
} receiveValue: { [weak self] response in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
@@ -60,6 +58,16 @@ final class MainHomeFollowingViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
.store(in: &subscription)
|
.store(in: &subscription)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func refresh() async {
|
||||||
|
if !isLoading {
|
||||||
|
fetchFollowing()
|
||||||
|
}
|
||||||
|
|
||||||
|
for await isLoading in $isLoading.values {
|
||||||
|
if !isLoading { return }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension HomeFollowingTabResponse {
|
private extension HomeFollowingTabResponse {
|
||||||
|
|||||||
@@ -15,10 +15,9 @@ struct MainHomeRankingView: View {
|
|||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
if viewModel.isLoading && !viewModel.hasLoaded {
|
if viewModel.isLoading && !viewModel.hasLoaded {
|
||||||
ProgressView()
|
loadingContent
|
||||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
|
||||||
} else if viewModel.items.isEmpty {
|
} else if viewModel.items.isEmpty {
|
||||||
MainHomeRankingEmptyStateView()
|
emptyContent
|
||||||
} else {
|
} else {
|
||||||
rankingContent
|
rankingContent
|
||||||
}
|
}
|
||||||
@@ -30,8 +29,23 @@ struct MainHomeRankingView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var loadingContent: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
LottieRefreshableScrollView(action: {
|
||||||
|
await viewModel.refresh()
|
||||||
|
}) {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(minHeight: geometry.size.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var rankingContent: some View {
|
private var rankingContent: some View {
|
||||||
ScrollView(showsIndicators: false) {
|
LottieRefreshableScrollView(action: {
|
||||||
|
await viewModel.refresh()
|
||||||
|
}) {
|
||||||
VStack(alignment: .leading, spacing: SodaSpacing.s8) {
|
VStack(alignment: .leading, spacing: SodaSpacing.s8) {
|
||||||
ForEach(firstRankItems) { item in
|
ForEach(firstRankItems) { item in
|
||||||
MainHomeRankingCard(
|
MainHomeRankingCard(
|
||||||
@@ -85,6 +99,17 @@ struct MainHomeRankingView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var emptyContent: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
LottieRefreshableScrollView(action: {
|
||||||
|
await viewModel.refresh()
|
||||||
|
}) {
|
||||||
|
MainHomeRankingEmptyStateView()
|
||||||
|
.frame(minHeight: geometry.size.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var twoColumns: [GridItem] {
|
private var twoColumns: [GridItem] {
|
||||||
[GridItem(.flexible()), GridItem(.flexible())]
|
[GridItem(.flexible()), GridItem(.flexible())]
|
||||||
}
|
}
|
||||||
|
|||||||