Files
suixinkan_ios_new/suixinkan/Features/Live/Views/LiveManagementViews.swift
汉秋 703078352c 从 iOS 17 Observation 迁移至 iOS 16 兼容的 Combine 架构
将最低部署版本降至 iOS 16,以 ObservableObject 替换 @Observable,新增导航与 UI 兼容层,并补充登录冒烟 UI 测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 10:16:35 +08:00

691 lines
28 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// LiveManagementViews.swift
// suixinkan
//
// Created by Codex on 2026/6/25.
//
import AVKit
import SwiftUI
///
struct LiveManagementView: View {
@EnvironmentObject private var accountContext: AccountContext
@Environment(\.liveAPI) private var liveAPI
@EnvironmentObject private var toastCenter: ToastCenter
@Environment(\.globalLoading) private var globalLoading
@StateObject private var viewModel = LiveManagementViewModel()
@State private var showAddPage = false
var body: some View {
VStack(spacing: 0) {
summarySection
ScrollView {
LazyVStack(spacing: AppMetrics.Spacing.medium) {
if viewModel.items.isEmpty {
AppContentUnavailableView("暂无直播", systemImage: "dot.radiowaves.left.and.right")
.frame(maxWidth: .infinity, minHeight: 280)
} else {
ForEach(viewModel.items) { item in
LiveManagementCard(
item: item,
onCopyURL: {
UIPasteboard.general.string = item.pushUrl
toastCenter.show("推流地址已复制")
},
onControl: { Task { await control(item) } },
onFinish: { Task { await finish(item) } },
detail: {
LiveDetailView(initialDetail: item) {
Task { await reload(showLoading: false) }
}
}
)
.onAppear {
guard item.id == viewModel.items.last?.id else { return }
Task { await viewModel.loadMore(api: liveAPI, scenicId: accountContext.currentScenic?.id) }
}
}
}
if viewModel.loadingMore {
ProgressView()
.padding(.vertical, AppMetrics.Spacing.small)
}
Spacer(minLength: 80)
}
.padding(.horizontal, AppMetrics.Spacing.pageHorizontal)
.padding(.vertical, AppMetrics.Spacing.medium)
}
Button {
showAddPage = true
} label: {
Label("添加直播", systemImage: "plus.circle.fill")
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
.frame(maxWidth: .infinity)
.frame(height: AppMetrics.ControlSize.primaryButtonHeight)
}
.buttonStyle(.borderedProminent)
.padding(.horizontal, AppMetrics.Spacing.pageHorizontal)
.padding(.vertical, AppMetrics.Spacing.medium)
.background(.white)
}
.background(Color(hex: 0xF5F7FA))
.navigationTitle("直播管理")
.navigationBarTitleDisplayMode(.inline)
.refreshable { await reload(showLoading: false) }
.task(id: accountContext.currentScenic?.id) {
await reload(showLoading: viewModel.items.isEmpty)
}
.navigationDestination(isPresented: $showAddPage) {
LiveAddView {
showAddPage = false
Task { await reload(showLoading: false) }
}
}
}
private var summarySection: some View {
HStack(spacing: AppMetrics.Spacing.small) {
LiveSummaryChip(title: "总直播", value: "\(viewModel.items.count)", color: AppDesign.primary)
LiveSummaryChip(title: "进行中", value: "\(viewModel.liveRunningCount)", color: AppDesign.success)
LiveSummaryChip(title: "已结束", value: "\(viewModel.liveFinishedCount)", color: Color(hex: 0x64748B))
}
.padding(AppMetrics.Spacing.medium)
.background(.white)
}
private func reload(showLoading: Bool) async {
await globalLoading.withOptionalLoading(showLoading && accountContext.currentScenic?.id != nil) {
await viewModel.reload(api: liveAPI, scenicId: accountContext.currentScenic?.id, showLoading: false)
}
if let message = viewModel.errorMessage {
toastCenter.show(message)
}
}
private func control(_ item: LiveEntity) async {
do {
try await viewModel.control(api: liveAPI, item: item, scenicId: accountContext.currentScenic?.id)
} catch {
toastCenter.show(error.localizedDescription)
}
}
private func finish(_ item: LiveEntity) async {
do {
try await viewModel.finish(api: liveAPI, item: item, scenicId: accountContext.currentScenic?.id)
} catch {
toastCenter.show(error.localizedDescription)
}
}
}
private struct LiveSummaryChip: View {
let title: String
let value: String
let color: Color
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.textSecondary)
Text(value)
.font(.system(size: AppMetrics.FontSize.body, weight: .bold))
.foregroundStyle(color)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, AppMetrics.Spacing.small)
.padding(.vertical, AppMetrics.Spacing.small)
.background(Color(hex: 0xF8FAFC), in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.input))
}
}
private struct LiveManagementCard<Detail: View>: View {
let item: LiveEntity
let onCopyURL: () -> Void
let onControl: () -> Void
let onFinish: () -> Void
@ViewBuilder let detail: () -> Detail
var body: some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) {
HStack(spacing: AppMetrics.Spacing.medium) {
RemoteImage(urlString: item.coverImg, contentMode: .fill) {
RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.input)
.fill(Color(hex: 0xE8EEF8))
.overlay {
Image(systemName: "play.rectangle.fill")
.font(.system(size: 28, weight: .semibold))
.foregroundStyle(AppDesign.primary.opacity(0.75))
}
}
.frame(width: 118, height: 72)
.clipShape(RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.input))
VStack(alignment: .leading, spacing: 5) {
Text(item.title.liveNonEmpty ?? "暂无标题")
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
.foregroundStyle(AppDesign.textPrimary)
.lineLimit(1)
Text("观看:\(item.viewsCount)")
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.textSecondary)
Text("时长:\(item.duration.liveDurationText)")
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.textSecondary)
Text(item.displayStatus)
.font(.system(size: AppMetrics.FontSize.caption, weight: .semibold))
.foregroundStyle(statusColor)
}
Spacer(minLength: 0)
}
HStack(spacing: AppMetrics.Spacing.small) {
NavigationLink(destination: detail) {
LiveSmallAction(title: "详情", color: AppDesign.primary)
}
.buttonStyle(.plain)
Button(action: onControl) {
LiveSmallAction(title: item.status == 2 ? "暂停" : "开始", color: Color(hex: 0xFF7B00))
}
.disabled(item.status == 3)
Button(action: onFinish) {
LiveSmallAction(title: "结束", color: Color(hex: 0xEF4444))
}
.disabled(item.status == 3)
Button(action: onCopyURL) {
LiveSmallAction(title: "复制地址", color: AppDesign.success)
}
.disabled(item.pushUrl.liveTrimmed.isEmpty)
}
}
.padding(AppMetrics.Spacing.medium)
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
private var statusColor: Color {
switch item.status {
case 2: AppDesign.success
case 3: Color(hex: 0x64748B)
default: AppDesign.primary
}
}
}
private struct LiveSmallAction: View {
let title: String
let color: Color
var body: some View {
Text(title)
.font(.system(size: AppMetrics.FontSize.caption, weight: .semibold))
.foregroundStyle(color)
.frame(maxWidth: .infinity)
.frame(height: 32)
.background(color.opacity(0.12), in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.input))
}
}
///
struct LiveAddView: View {
@EnvironmentObject private var accountContext: AccountContext
@Environment(\.liveAPI) private var liveAPI
@EnvironmentObject private var toastCenter: ToastCenter
@Environment(\.dismiss) private var dismiss
@State private var title = ""
@State private var coverURL = ""
@State private var submitting = false
let onCreated: () -> Void
var body: some View {
VStack(spacing: 0) {
ScrollView {
VStack(spacing: AppMetrics.Spacing.medium) {
previewSection
formSection
}
.padding(AppMetrics.Spacing.pageHorizontal)
}
Button {
Task { await submit() }
} label: {
HStack {
if submitting { ProgressView().tint(.white) }
Text(submitting ? "创建中..." : "确认添加")
}
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
.frame(maxWidth: .infinity)
.frame(height: AppMetrics.ControlSize.primaryButtonHeight)
}
.buttonStyle(.borderedProminent)
.disabled(!canSubmit)
.padding(AppMetrics.Spacing.pageHorizontal)
.background(.white)
}
.background(Color(hex: 0xF5F7FA))
.navigationTitle("添加直播")
.navigationBarTitleDisplayMode(.inline)
}
private var canSubmit: Bool {
!submitting && !title.liveTrimmed.isEmpty && coverURL.liveTrimmed.liveIsHTTPURL
}
private var previewSection: some View {
ZStack(alignment: .bottomLeading) {
if coverURL.liveTrimmed.liveIsHTTPURL {
RemoteImage(urlString: coverURL.liveTrimmed, contentMode: .fill) {
previewPlaceholder
}
} else {
previewPlaceholder
}
VStack(alignment: .leading, spacing: AppMetrics.Spacing.xSmall) {
Text("直播封面")
.font(.system(size: AppMetrics.FontSize.caption, weight: .semibold))
.foregroundStyle(.white.opacity(0.9))
Text(title.liveTrimmed.isEmpty ? "输入标题后预览展示效果" : title.liveTrimmed)
.font(.system(size: AppMetrics.FontSize.title2, weight: .bold))
.foregroundStyle(.white)
.lineLimit(2)
}
.padding(AppMetrics.Spacing.medium)
.frame(maxWidth: .infinity, alignment: .leading)
.background(LinearGradient(colors: [.clear, .black.opacity(0.72)], startPoint: .top, endPoint: .bottom))
}
.frame(height: 210)
.clipShape(RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
private var previewPlaceholder: some View {
LinearGradient(colors: [Color(hex: 0x0F274A), AppDesign.primary], startPoint: .topLeading, endPoint: .bottomTrailing)
.overlay {
Image(systemName: "play.rectangle.fill")
.font(.system(size: 46, weight: .semibold))
.foregroundStyle(.white.opacity(0.72))
}
}
private var formSection: some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.medium) {
Text("直播信息")
.font(.system(size: AppMetrics.FontSize.title3, weight: .bold))
TextField("请输入直播标题", text: $title)
.appInputFieldStyle(cornerRadius: AppMetrics.CornerRadius.input, minHeight: AppMetrics.ControlSize.inputHeight)
TextField("请输入 http/https 图片地址", text: $coverURL)
.textInputAutocapitalization(.never)
.autocorrectionDisabled(true)
.keyboardType(.URL)
.appInputFieldStyle(cornerRadius: AppMetrics.CornerRadius.input, minHeight: AppMetrics.ControlSize.inputHeight)
if !coverURL.liveTrimmed.isEmpty && !coverURL.liveTrimmed.liveIsHTTPURL {
Text("封面图地址需以 http:// 或 https:// 开头")
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.warning)
}
}
.padding(AppMetrics.Spacing.medium)
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
private func submit() async {
guard canSubmit else { return }
guard let scenicId = accountContext.currentScenic?.id else {
toastCenter.show("当前账号缺少景区信息")
return
}
submitting = true
defer { submitting = false }
do {
try await liveAPI.liveCreate(LiveCreateRequest(scenicId: String(scenicId), title: title.liveTrimmed, coverImg: coverURL.liveTrimmed))
onCreated()
dismiss()
} catch {
toastCenter.show(error.localizedDescription)
}
}
}
///
struct LiveDetailView: View {
@Environment(\.liveAPI) private var liveAPI
@EnvironmentObject private var toastCenter: ToastCenter
@Environment(\.globalLoading) private var globalLoading
@StateObject private var viewModel: LiveDetailViewModel
@StateObject private var playbackViewModel: LivePlaybackViewModel
@StateObject private var pushReadinessViewModel = LivePushReadinessViewModel()
@State private var copied = false
let onChanged: () -> Void
init(initialDetail: LiveEntity, onChanged: @escaping () -> Void) {
_viewModel = StateObject(wrappedValue: LiveDetailViewModel(detail: initialDetail))
_playbackViewModel = StateObject(wrappedValue: LivePlaybackViewModel(live: initialDetail))
self.onChanged = onChanged
}
var body: some View {
VStack(spacing: 0) {
ScrollView {
VStack(spacing: AppMetrics.Spacing.medium) {
playbackSection
infoSection
pushURLSection
pushModeSection
pushReadinessSection
Spacer(minLength: 80)
}
.padding(AppMetrics.Spacing.pageHorizontal)
}
bottomBar
}
.background(Color(hex: 0xF5F7FA))
.navigationTitle("直播信息")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("刷新") {
Task { await refresh(showLoading: true) }
}
.disabled(viewModel.loading || viewModel.actionInFlight)
}
}
.task {
pushReadinessViewModel.configure(pushURL: viewModel.detail.pushUrl)
pushReadinessViewModel.startMonitoring()
await pushReadinessViewModel.refreshPermissions()
await refresh(showLoading: false)
}
.onDisappear {
playbackViewModel.release()
Task { await pushReadinessViewModel.dispose() }
}
}
private var playbackSection: some View {
ZStack(alignment: .bottomLeading) {
if let player = playbackViewModel.player, playbackViewModel.playableURL != nil {
VideoPlayer(player: player)
.background(Color.black)
} else {
RemoteImage(urlString: viewModel.detail.coverImg, contentMode: .fill) {
LinearGradient(colors: [Color(hex: 0x0F274A), AppDesign.primary], startPoint: .topLeading, endPoint: .bottomTrailing)
.overlay {
Image(systemName: "dot.radiowaves.left.and.right")
.font(.system(size: 42, weight: .semibold))
.foregroundStyle(.white.opacity(0.72))
}
}
}
VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) {
Text(viewModel.detail.displayStatus)
.font(.system(size: AppMetrics.FontSize.caption, weight: .semibold))
.foregroundStyle(.white)
.padding(.horizontal, AppMetrics.Spacing.small)
.frame(height: 26)
.background(statusColor.opacity(0.92), in: Capsule())
Text(viewModel.detail.title.liveNonEmpty ?? "暂无标题")
.font(.system(size: AppMetrics.FontSize.title, weight: .bold))
.foregroundStyle(.white)
.lineLimit(2)
Text(playbackHint)
.font(.system(size: AppMetrics.FontSize.caption, weight: .medium))
.foregroundStyle(.white.opacity(0.86))
.lineLimit(2)
HStack(spacing: AppMetrics.Spacing.small) {
if playbackViewModel.playableURL != nil {
Button {
playbackViewModel.isPlaying ? playbackViewModel.pause() : playbackViewModel.play()
} label: {
Label(playbackViewModel.isPlaying ? "暂停" : "播放", systemImage: playbackViewModel.isPlaying ? "pause.fill" : "play.fill")
}
Button {
playbackViewModel.reload()
} label: {
Label("重载", systemImage: "arrow.clockwise")
}
}
if let url = playbackViewModel.playableURL {
Link(destination: url) {
Label("打开", systemImage: "safari")
}
}
}
.font(.system(size: AppMetrics.FontSize.caption, weight: .semibold))
.buttonStyle(.borderedProminent)
.tint(.white.opacity(0.22))
}
.padding(AppMetrics.Spacing.medium)
.frame(maxWidth: .infinity, alignment: .leading)
.background(LinearGradient(colors: [.clear, .black.opacity(0.72)], startPoint: .top, endPoint: .bottom))
}
.frame(height: 210)
.clipShape(RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
private var playbackHint: String {
if playbackViewModel.playableURL != nil {
return "已检测到可播放地址"
}
if viewModel.detail.pushUrl.liveTrimmed.lowercased().hasPrefix("rtmp://") {
return "当前仅有 RTMP 推流地址,系统播放器不可直接播放"
}
return "暂无可播放直播地址"
}
private var infoSection: some View {
liveDetailCard("直播信息") {
LiveInfoRow(title: "标题", value: viewModel.detail.title.liveNonEmpty ?? "--")
LiveInfoRow(title: "状态", value: viewModel.detail.displayStatus)
LiveInfoRow(title: "直播时长", value: viewModel.detail.duration.liveDurationText)
LiveInfoRow(title: "观看人次", value: "\(viewModel.detail.viewsCount)")
}
}
private var pushURLSection: some View {
liveDetailCard("推流地址") {
Text("该地址用于 OBS 或第三方推流工具;当前版本不在 App 内采集摄像头/麦克风并推流。")
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.textSecondary)
Text(viewModel.detail.pushUrl.liveNonEmpty ?? "--")
.font(.system(size: AppMetrics.FontSize.subheadline))
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(AppMetrics.Spacing.medium)
.background(Color(hex: 0xF8FAFC), in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.input))
Button {
UIPasteboard.general.string = viewModel.detail.pushUrl
copied = true
toastCenter.show("推流地址已复制")
} label: {
Label(copied ? "已复制推流地址" : "复制推流地址", systemImage: "doc.on.doc")
.frame(maxWidth: .infinity, alignment: .leading)
}
.disabled(viewModel.detail.pushUrl.liveTrimmed.isEmpty)
}
}
private var pushModeSection: some View {
liveDetailCard("推流模式") {
HStack(spacing: AppMetrics.Spacing.small) {
pushModeButton("清晰度优先", mode: 1)
pushModeButton("流畅度优先", mode: 2)
}
}
}
private var pushReadinessSection: some View {
liveDetailCard("推流诊断") {
LiveInfoRow(title: "相机权限", value: pushReadinessViewModel.cameraPermission.displayText)
LiveInfoRow(title: "麦克风权限", value: pushReadinessViewModel.microphonePermission.displayText)
LiveInfoRow(title: "网络状态", value: pushReadinessViewModel.networkState.displayText)
LiveInfoRow(title: "推流 SDK", value: pushReadinessViewModel.adapterName)
LiveInfoRow(title: "SDK 状态", value: pushReadinessViewModel.sdkStatusText)
Text("此处仅做权限、网络和 SDK 接入状态诊断;开始、暂停、结束仍只控制 manual-live 业务状态。")
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.textSecondary)
if let message = pushReadinessViewModel.errorMessage {
Text(message)
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(Color(hex: 0xEF4444))
}
HStack(spacing: AppMetrics.Spacing.small) {
Button {
Task { await pushReadinessViewModel.refreshPermissions() }
} label: {
Label("重新检查", systemImage: "checkmark.shield")
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
Button {
diagnosePush()
} label: {
Label("检查 SDK", systemImage: "antenna.radiowaves.left.and.right")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
}
}
}
private var bottomBar: some View {
HStack(spacing: AppMetrics.Spacing.medium) {
Button {
Task { await control() }
} label: {
Text(viewModel.detail.status == 2 ? "暂停直播" : "开始直播")
.frame(maxWidth: .infinity)
.frame(height: AppMetrics.ControlSize.primaryButtonHeight)
}
.buttonStyle(.borderedProminent)
.disabled(viewModel.detail.status == 3 || viewModel.actionInFlight)
Button(role: .destructive) {
Task { await finish() }
} label: {
Text("结束直播")
.frame(maxWidth: .infinity)
.frame(height: AppMetrics.ControlSize.primaryButtonHeight)
}
.buttonStyle(.bordered)
.disabled(viewModel.detail.status == 3 || viewModel.actionInFlight)
}
.padding(AppMetrics.Spacing.pageHorizontal)
.background(.white)
}
private var statusColor: Color {
switch viewModel.detail.status {
case 2: AppDesign.success
case 3: Color(hex: 0x64748B)
default: AppDesign.primary
}
}
private func liveDetailCard<Content: View>(_ title: String, @ViewBuilder content: () -> Content) -> some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.medium) {
Text(title)
.font(.system(size: AppMetrics.FontSize.title3, weight: .bold))
.foregroundStyle(AppDesign.textPrimary)
content()
}
.padding(AppMetrics.Spacing.medium)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
private func pushModeButton(_ title: String, mode: Int) -> some View {
Button {
Task { await setPushMode(mode) }
} label: {
Text(title)
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold))
.foregroundStyle(viewModel.detail.manualPushMode == mode ? .white : AppDesign.textSecondary)
.frame(maxWidth: .infinity)
.frame(height: 46)
.background(viewModel.detail.manualPushMode == mode ? AppDesign.primary : Color(hex: 0xEEF2F7), in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.input))
}
.disabled(viewModel.actionInFlight || viewModel.detail.manualPushMode == mode)
.buttonStyle(.plain)
}
private func refresh(showLoading: Bool) async {
await globalLoading.withOptionalLoading(showLoading) {
await viewModel.refresh(api: liveAPI, showLoading: false)
}
syncMediaState()
if let message = viewModel.errorMessage {
toastCenter.show(message)
}
}
private func control() async {
do {
try await viewModel.control(api: liveAPI)
onChanged()
} catch {
toastCenter.show(error.localizedDescription)
}
}
private func finish() async {
do {
try await viewModel.finish(api: liveAPI)
onChanged()
} catch {
toastCenter.show(error.localizedDescription)
}
}
private func setPushMode(_ mode: Int) async {
do {
try await viewModel.setPushMode(api: liveAPI, mode: mode)
onChanged()
} catch {
toastCenter.show(error.localizedDescription)
}
}
private func diagnosePush() {
pushReadinessViewModel.configure(pushURL: viewModel.detail.pushUrl)
do {
try pushReadinessViewModel.runDiagnostics()
toastCenter.show("推流诊断通过")
} catch {
toastCenter.show(error.localizedDescription)
}
}
private func syncMediaState() {
playbackViewModel.load(live: viewModel.detail)
pushReadinessViewModel.configure(pushURL: viewModel.detail.pushUrl)
}
}
private struct LiveInfoRow: View {
let title: String
let value: String
var body: some View {
HStack(alignment: .top, spacing: AppMetrics.Spacing.medium) {
Text(title)
.foregroundStyle(AppDesign.textSecondary)
.frame(width: 72, alignment: .leading)
Text(value)
.foregroundStyle(AppDesign.textPrimary)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity, alignment: .trailing)
}
.font(.system(size: AppMetrics.FontSize.subheadline))
}
}
private extension String {
var liveIsHTTPURL: Bool {
let lower = liveTrimmed.lowercased()
return lower.hasPrefix("http://") || lower.hasPrefix("https://")
}
}