Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
S
Sailassist
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
54
Merge Requests
54
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
gr-ssv
Sailassist
Commits
65b05c9e
Commit
65b05c9e
authored
Oct 01, 2025
by
shigemi miura
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
動画処理が出来ないバージョン
parent
8cec0939
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
388 additions
and
100 deletions
+388
-100
project.pbxproj
Seilassist/Sailassist.xcodeproj/project.pbxproj
+6
-2
Imagepicker.swift
Seilassist/Sailassist/Chat/Imagepicker.swift
+12
-19
VideoQualityPreset.swift
Seilassist/Sailassist/Chat/VideoQualityPreset.swift
+76
-0
CameraView.swift
Seilassist/Sailassist/Chat/View/CameraView.swift
+0
-1
ChatInputView.swift
Seilassist/Sailassist/Chat/View/ChatInputView.swift
+17
-0
MyChatContentView.swift
Seilassist/Sailassist/Chat/View/MyChatContentView.swift
+66
-44
OtherChatContentView.swift
Seilassist/Sailassist/Chat/View/OtherChatContentView.swift
+15
-3
ChatInputViewModel.swift
...assist/Sailassist/Chat/ViewModel/ChatInputViewModel.swift
+124
-27
ResGetMessages.swift
Seilassist/Sailassist/Json/ResGetMessages.swift
+3
-0
SailassistApp.swift
Seilassist/Sailassist/SailassistApp.swift
+0
-1
SessionUploadImage.swift
Seilassist/Sailassist/ServerSession/SessionUploadImage.swift
+69
-3
No files found.
Seilassist/Sailassist.xcodeproj/project.pbxproj
View file @
65b05c9e
...
@@ -149,6 +149,7 @@
...
@@ -149,6 +149,7 @@
D5E008762B2ADD5900C4070A
/* MenuManualRADARView.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
D5E008752B2ADD5900C4070A
/* MenuManualRADARView.swift */
;
};
D5E008762B2ADD5900C4070A
/* MenuManualRADARView.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
D5E008752B2ADD5900C4070A
/* MenuManualRADARView.swift */
;
};
D5E008782B2B022200C4070A
/* MenuAboutAppView.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
D5E008772B2B022200C4070A
/* MenuAboutAppView.swift */
;
};
D5E008782B2B022200C4070A
/* MenuAboutAppView.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
D5E008772B2B022200C4070A
/* MenuAboutAppView.swift */
;
};
D5E03A672B04484D00D65FCE
/* SessionTaskList.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
D5E03A662B04484D00D65FCE
/* SessionTaskList.swift */
;
};
D5E03A672B04484D00D65FCE
/* SessionTaskList.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
D5E03A662B04484D00D65FCE
/* SessionTaskList.swift */
;
};
D5F969BD2E8AAD4F005662B0
/* VideoQualityPreset.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
D5F969BC2E8AAD3C005662B0
/* VideoQualityPreset.swift */
;
};
D5FCEF552B478985009A81D0
/* ResChatMessage.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
D5FCEF542B478985009A81D0
/* ResChatMessage.swift */
;
};
D5FCEF552B478985009A81D0
/* ResChatMessage.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
D5FCEF542B478985009A81D0
/* ResChatMessage.swift */
;
};
D5FCEF572B4789A8009A81D0
/* ResAckMessage.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
D5FCEF562B4789A8009A81D0
/* ResAckMessage.swift */
;
};
D5FCEF572B4789A8009A81D0
/* ResAckMessage.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
D5FCEF562B4789A8009A81D0
/* ResAckMessage.swift */
;
};
D5FCEF592B4789D2009A81D0
/* ResChatMode.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
D5FCEF582B4789D2009A81D0
/* ResChatMode.swift */
;
};
D5FCEF592B4789D2009A81D0
/* ResChatMode.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
D5FCEF582B4789D2009A81D0
/* ResChatMode.swift */
;
};
...
@@ -339,6 +340,7 @@
...
@@ -339,6 +340,7 @@
D5E008752B2ADD5900C4070A
/* MenuManualRADARView.swift */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
sourcecode.swift
;
name
=
MenuManualRADARView.swift
;
path
=
Sailassist/Menu/View/MenuManualRADARView.swift
;
sourceTree
=
SOURCE_ROOT
;
};
D5E008752B2ADD5900C4070A
/* MenuManualRADARView.swift */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
sourcecode.swift
;
name
=
MenuManualRADARView.swift
;
path
=
Sailassist/Menu/View/MenuManualRADARView.swift
;
sourceTree
=
SOURCE_ROOT
;
};
D5E008772B2B022200C4070A
/* MenuAboutAppView.swift */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
sourcecode.swift
;
name
=
MenuAboutAppView.swift
;
path
=
Sailassist/Menu/View/MenuAboutAppView.swift
;
sourceTree
=
SOURCE_ROOT
;
};
D5E008772B2B022200C4070A
/* MenuAboutAppView.swift */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
sourcecode.swift
;
name
=
MenuAboutAppView.swift
;
path
=
Sailassist/Menu/View/MenuAboutAppView.swift
;
sourceTree
=
SOURCE_ROOT
;
};
D5E03A662B04484D00D65FCE
/* SessionTaskList.swift */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
sourcecode.swift
;
name
=
SessionTaskList.swift
;
path
=
Sailassist/ServerSession/SessionTaskList.swift
;
sourceTree
=
SOURCE_ROOT
;
};
D5E03A662B04484D00D65FCE
/* SessionTaskList.swift */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
sourcecode.swift
;
name
=
SessionTaskList.swift
;
path
=
Sailassist/ServerSession/SessionTaskList.swift
;
sourceTree
=
SOURCE_ROOT
;
};
D5F969BC2E8AAD3C005662B0
/* VideoQualityPreset.swift */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
sourcecode.swift
;
path
=
VideoQualityPreset.swift
;
sourceTree
=
"<group>"
;
};
D5FCEF542B478985009A81D0
/* ResChatMessage.swift */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
sourcecode.swift
;
name
=
ResChatMessage.swift
;
path
=
Sailassist/Json/ResChatMessage.swift
;
sourceTree
=
SOURCE_ROOT
;
};
D5FCEF542B478985009A81D0
/* ResChatMessage.swift */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
sourcecode.swift
;
name
=
ResChatMessage.swift
;
path
=
Sailassist/Json/ResChatMessage.swift
;
sourceTree
=
SOURCE_ROOT
;
};
D5FCEF562B4789A8009A81D0
/* ResAckMessage.swift */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
sourcecode.swift
;
name
=
ResAckMessage.swift
;
path
=
Sailassist/Json/ResAckMessage.swift
;
sourceTree
=
SOURCE_ROOT
;
};
D5FCEF562B4789A8009A81D0
/* ResAckMessage.swift */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
sourcecode.swift
;
name
=
ResAckMessage.swift
;
path
=
Sailassist/Json/ResAckMessage.swift
;
sourceTree
=
SOURCE_ROOT
;
};
D5FCEF582B4789D2009A81D0
/* ResChatMode.swift */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
sourcecode.swift
;
name
=
ResChatMode.swift
;
path
=
Sailassist/Json/ResChatMode.swift
;
sourceTree
=
SOURCE_ROOT
;
};
D5FCEF582B4789D2009A81D0
/* ResChatMode.swift */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
sourcecode.swift
;
name
=
ResChatMode.swift
;
path
=
Sailassist/Json/ResChatMode.swift
;
sourceTree
=
SOURCE_ROOT
;
};
...
@@ -541,6 +543,7 @@
...
@@ -541,6 +543,7 @@
D5AF8A4B2E88CDF600BECA22
/* ViewModel */
,
D5AF8A4B2E88CDF600BECA22
/* ViewModel */
,
D5258CA42B036F0700365276
/* GetMessage.swift */
,
D5258CA42B036F0700365276
/* GetMessage.swift */
,
D55186122E7E914C004CD8BD
/* ImageQualityPreset.swift */
,
D55186122E7E914C004CD8BD
/* ImageQualityPreset.swift */
,
D5F969BC2E8AAD3C005662B0
/* VideoQualityPreset.swift */
,
D524DAF72B5A6D3600A399DD
/* Imagepicker.swift */
,
D524DAF72B5A6D3600A399DD
/* Imagepicker.swift */
,
);
);
path
=
Chat
;
path
=
Chat
;
...
@@ -665,8 +668,6 @@
...
@@ -665,8 +668,6 @@
children
=
(
children
=
(
020B98622ADD14E40029DE4C
/* ChatView.swift */
,
020B98622ADD14E40029DE4C
/* ChatView.swift */
,
02A1DE2E2AFB4AA0005BCF55
/* ChatInputView.swift */
,
02A1DE2E2AFB4AA0005BCF55
/* ChatInputView.swift */
,
D57405052E827C35001C74DF
/* ChatUrlVideoView.swift */
,
D5A4C2522E7AB46B00642D7D
/* SpeechRecognizer.swift */
,
02C3E5D02AFCC16800AF7837
/* ChatTitleView.swift */
,
02C3E5D02AFCC16800AF7837
/* ChatTitleView.swift */
,
02A1DE302AFB61D8005BCF55
/* MyChatContentView.swift */
,
02A1DE302AFB61D8005BCF55
/* MyChatContentView.swift */
,
02C3E5CD2AFCA04700AF7837
/* OtherChatContentView.swift */
,
02C3E5CD2AFCA04700AF7837
/* OtherChatContentView.swift */
,
...
@@ -674,7 +675,9 @@
...
@@ -674,7 +675,9 @@
02C3E6082AFDF30000AF7837
/* ChatMemberView.swift */
,
02C3E6082AFDF30000AF7837
/* ChatMemberView.swift */
,
D524DAF92B5A6F5F00A399DD
/* CameraView.swift */
,
D524DAF92B5A6F5F00A399DD
/* CameraView.swift */
,
D5598B772C435A5C00611AE0
/* ChatUrlImageView.swift */
,
D5598B772C435A5C00611AE0
/* ChatUrlImageView.swift */
,
D57405052E827C35001C74DF
/* ChatUrlVideoView.swift */
,
D5598B792C435C4500611AE0
/* ChatUrlRawImageView.swift */
,
D5598B792C435C4500611AE0
/* ChatUrlRawImageView.swift */
,
D5A4C2522E7AB46B00642D7D
/* SpeechRecognizer.swift */
,
);
);
path
=
View
;
path
=
View
;
sourceTree
=
"<group>"
;
sourceTree
=
"<group>"
;
...
@@ -1148,6 +1151,7 @@
...
@@ -1148,6 +1151,7 @@
020B98512AD9189A0029DE4C
/* InputIdPassWordView.swift in Sources */
,
020B98512AD9189A0029DE4C
/* InputIdPassWordView.swift in Sources */
,
D52C2C0A2B91BF24003B286C
/* InformationView.swift in Sources */
,
D52C2C0A2B91BF24003B286C
/* InformationView.swift in Sources */
,
D52D213A2AEBAC0500324D58
/* HttpRequestType.swift in Sources */
,
D52D213A2AEBAC0500324D58
/* HttpRequestType.swift in Sources */
,
D5F969BD2E8AAD4F005662B0
/* VideoQualityPreset.swift in Sources */
,
D53B97262B341867000B3D29
/* AboutAppView.swift in Sources */
,
D53B97262B341867000B3D29
/* AboutAppView.swift in Sources */
,
D5258CA12B03593500365276
/* SessionMonitoringRoute.swift in Sources */
,
D5258CA12B03593500365276
/* SessionMonitoringRoute.swift in Sources */
,
D5E03A672B04484D00D65FCE
/* SessionTaskList.swift in Sources */
,
D5E03A672B04484D00D65FCE
/* SessionTaskList.swift in Sources */
,
...
...
Seilassist/Sailassist/Chat/Imagepicker.swift
View file @
65b05c9e
//
// Imagepicker.swift
// Sailassist
//
// Created by 三浦薫巳 on 2024/01/19.
//
import
SwiftUI
import
SwiftUI
import
AVFoundation
struct
Imagepicker
:
UIViewControllerRepresentable
{
struct
Imagepicker
:
UIViewControllerRepresentable
{
@Binding
var
show
:
Bool
@Binding
var
show
:
Bool
...
@@ -30,6 +25,7 @@ struct Imagepicker : UIViewControllerRepresentable {
...
@@ -30,6 +25,7 @@ struct Imagepicker : UIViewControllerRepresentable {
class
Coordinator
:
NSObject
,
UIImagePickerControllerDelegate
,
UINavigationControllerDelegate
{
class
Coordinator
:
NSObject
,
UIImagePickerControllerDelegate
,
UINavigationControllerDelegate
{
var
parent
:
Imagepicker
var
parent
:
Imagepicker
var
selectedPreset
:
VideoQualityPreset
=
.
medium
init
(
parent
:
Imagepicker
){
init
(
parent
:
Imagepicker
){
self
.
parent
=
parent
self
.
parent
=
parent
...
@@ -40,7 +36,7 @@ struct Imagepicker : UIViewControllerRepresentable {
...
@@ -40,7 +36,7 @@ struct Imagepicker : UIViewControllerRepresentable {
self
.
parent
.
show
.
toggle
()
self
.
parent
.
show
.
toggle
()
}
}
//MARK: - Use Photo
//MARK: - Use Photo
/ Movie
func
imagePickerController
(
_
picker
:
UIImagePickerController
,
didFinishPickingMediaWithInfo
info
:
[
UIImagePickerController
.
InfoKey
:
Any
])
{
func
imagePickerController
(
_
picker
:
UIImagePickerController
,
didFinishPickingMediaWithInfo
info
:
[
UIImagePickerController
.
InfoKey
:
Any
])
{
if
let
image
=
info
[
.
originalImage
]
as?
UIImage
{
if
let
image
=
info
[
.
originalImage
]
as?
UIImage
{
//MARK: - 画像データのリサイズ
//MARK: - 画像データのリサイズ
...
@@ -55,29 +51,26 @@ struct Imagepicker : UIViewControllerRepresentable {
...
@@ -55,29 +51,26 @@ struct Imagepicker : UIViewControllerRepresentable {
}
}
}
else
{
}
else
{
DispatchQueue
.
main
.
async
{
DispatchQueue
.
main
.
async
{
print
(
"Failed to resize or compress image"
)
print
(
debug
:
"Failed to resize or compress image"
)
self
.
parent
.
show
.
toggle
()
self
.
parent
.
show
.
toggle
()
}
}
}
}
}
}
}
else
if
let
videoURL
=
info
[
.
mediaURL
]
as?
URL
{
}
else
if
let
videoURL
=
info
[
.
mediaURL
]
as?
URL
{
//MARK: - 動画データのリサイズ
//MARK: - 動画データのリサイズ
DispatchQueue
.
global
(
qos
:
.
userInitiated
)
.
async
{
compressVideo
(
inputURL
:
videoURL
,
preset
:
selectedPreset
)
{
compressedData
in
if
let
videoData
=
try
?
Data
(
contentsOf
:
videoURL
)
{
DispatchQueue
.
main
.
async
{
DispatchQueue
.
main
.
async
{
if
let
data
=
compressedData
{
self
.
parent
.
image
=
videoData
self
.
parent
.
image
=
data
self
.
parent
.
show
.
toggle
()
}
else
{
}
print
(
debug
:
"Failed to compress video"
)
}
else
{
DispatchQueue
.
main
.
async
{
print
(
"Failed to process video data"
)
self
.
parent
.
show
.
toggle
()
}
}
self
.
parent
.
show
.
toggle
()
}
}
}
}
}
else
{
}
else
{
DispatchQueue
.
main
.
async
{
DispatchQueue
.
main
.
async
{
print
(
"No valid media selected"
)
print
(
debug
:
"No valid media selected"
)
self
.
parent
.
show
.
toggle
()
self
.
parent
.
show
.
toggle
()
}
}
}
}
...
...
Seilassist/Sailassist/Chat/VideoQualityPreset.swift
0 → 100644
View file @
65b05c9e
import
Network
import
AVFoundation
enum
VideoQualityPreset
{
case
low
,
medium
,
high
var
exportPresetName
:
String
{
switch
self
{
case
.
low
:
return
AVAssetExportPresetLowQuality
case
.
medium
:
return
AVAssetExportPresetMediumQuality
case
.
high
:
return
AVAssetExportPresetHighestQuality
}
}
var
fileExtension
:
String
{
return
"mp4"
}
var
mimeType
:
String
{
return
"video/mp4"
}
}
func
isVideoSizeAcceptable
(
_
data
:
Data
,
maxSizeMB
:
Double
=
28.0
)
->
Bool
{
let
sizeMB
=
Double
(
data
.
count
)
/
(
1024.0
*
1024.0
)
return
sizeMB
<=
maxSizeMB
}
// MARK: - 動画圧縮処理
func
compressVideo
(
inputURL
:
URL
,
preset
:
VideoQualityPreset
,
completion
:
@escaping
(
Data
?)
->
Void
)
{
let
asset
=
AVAsset
(
url
:
inputURL
)
guard
let
exportSession
=
AVAssetExportSession
(
asset
:
asset
,
presetName
:
preset
.
exportPresetName
)
else
{
completion
(
nil
)
return
}
let
outputURL
=
FileManager
.
default
.
temporaryDirectory
.
appendingPathComponent
(
UUID
()
.
uuidString
+
".mp4"
)
exportSession
.
outputURL
=
outputURL
exportSession
.
outputFileType
=
.
mp4
exportSession
.
shouldOptimizeForNetworkUse
=
true
exportSession
.
exportAsynchronously
{
if
exportSession
.
status
==
.
completed
{
if
let
data
=
try
?
Data
(
contentsOf
:
outputURL
),
isVideoSizeAcceptable
(
data
)
{
completion
(
data
)
}
else
{
completion
(
nil
)
// サイズオーバー
}
}
else
{
completion
(
nil
)
}
}
}
func
selectPresetBasedOnNetwork
()
->
VideoQualityPreset
{
let
monitor
=
NWPathMonitor
()
var
selectedPreset
:
VideoQualityPreset
=
.
medium
monitor
.
pathUpdateHandler
=
{
path
in
if
path
.
status
==
.
satisfied
{
if
path
.
isExpensive
{
selectedPreset
=
.
low
// モバイル通信
}
else
{
selectedPreset
=
.
high
// Wi-Fi
}
}
else
{
selectedPreset
=
.
low
// オフラインまたは不安定
}
}
let
queue
=
DispatchQueue
(
label
:
"NetworkMonitor"
)
monitor
.
start
(
queue
:
queue
)
return
selectedPreset
}
Seilassist/Sailassist/Chat/View/CameraView.swift
View file @
65b05c9e
...
@@ -4,7 +4,6 @@
...
@@ -4,7 +4,6 @@
//
//
// Created by 三浦薫巳 on 2024/01/19.
// Created by 三浦薫巳 on 2024/01/19.
//
//
import
SwiftUI
import
SwiftUI
struct
CameraView
:
View
{
struct
CameraView
:
View
{
...
...
Seilassist/Sailassist/Chat/View/ChatInputView.swift
View file @
65b05c9e
...
@@ -136,6 +136,8 @@ struct ChatInputView: View {
...
@@ -136,6 +136,8 @@ struct ChatInputView: View {
if
let
image
=
UIImage
(
data
:
imageData
)
{
if
let
image
=
UIImage
(
data
:
imageData
)
{
viewModel
.
sendImageToTemporary
(
image
)
viewModel
.
sendImageToTemporary
(
image
)
}
else
if
!
imageData
.
isEmpty
{
viewModel
.
sendVideoToTemporary
(
imageData
)
}
}
})
{
})
{
CameraView
(
imageData
:
$
imageData
,
source
:
$
source
,
isActionSheet
:
.
constant
(
false
),
isImagePicker
:
$
viewModel
.
isImagePickerPresented
)
CameraView
(
imageData
:
$
imageData
,
source
:
$
source
,
isActionSheet
:
.
constant
(
false
),
isImagePicker
:
$
viewModel
.
isImagePickerPresented
)
...
@@ -206,6 +208,21 @@ struct ChatInputView: View {
...
@@ -206,6 +208,21 @@ struct ChatInputView: View {
}
message
:
{
}
message
:
{
Text
(
"Image upload failed. Would you like to try again?"
)
Text
(
"Image upload failed. Would you like to try again?"
)
}
}
.
alert
(
"Upload failed"
,
isPresented
:
$
viewModel
.
isRetryDialogPresented
)
{
Button
(
"retry"
)
{
if
let
videoToRetry
=
viewModel
.
failedUploadImage
{
viewModel
.
sendChatVideo
(
videoToRetry
)
}
}
Button
(
"Cancel"
,
role
:
.
cancel
)
{
viewModel
.
failedUploadImage
=
nil
viewModel
.
isRetryDialogPresented
=
false
SharingData
.
message
.
messages
.
removeAll
{
$0
.
messageId
.
caseInsensitiveCompare
(
viewModel
.
tempId
)
==
.
orderedSame
}
}
}
message
:
{
Text
(
"Video upload failed. Would you like to try again?"
)
}
}
}
}
}
...
...
Seilassist/Sailassist/Chat/View/MyChatContentView.swift
View file @
65b05c9e
...
@@ -6,67 +6,79 @@
...
@@ -6,67 +6,79 @@
//
//
import
SwiftUI
import
SwiftUI
import
AVKit
struct
MyChatContentView
:
View
{
struct
MyChatContentView
:
View
{
@State
private
var
isVideoPlayerPresented
=
false
var
message
:
ChatMessage
var
message
:
ChatMessage
var
onMediaLoaded
:
(()
->
Void
)?
=
nil
var
onMediaLoaded
:
(()
->
Void
)?
=
nil
var
body
:
some
View
{
var
body
:
some
View
{
HStack
{
HStack
{
Spacer
()
Spacer
()
VStack
(
alignment
:
.
trailing
,
spacing
:
6
)
{
VStack
(
alignment
:
.
trailing
,
spacing
:
6
)
{
Group
{
Group
{
if
let
msg
=
message
.
message
{
if
let
msg
=
message
.
message
{
if
msg
.
contains
(
".jpg"
)
||
msg
.
contains
(
".png"
)
{
if
msg
.
contains
(
".jpg"
)
||
msg
.
contains
(
".png"
)
{
ChatUrlImageView
(
imageUrl
:
msg
,
onLoad
:
onMediaLoaded
)
ChatUrlImageView
(
imageUrl
:
msg
,
onLoad
:
onMediaLoaded
)
}
else
if
msg
.
contains
(
".mp4"
)
||
msg
.
contains
(
".mov"
)
{
// .onTapGesture {
ChatUrlVideoView
(
imageUrl
:
msg
,
onLoad
:
onMediaLoaded
)
// if let videoUrl = message.videoUrl,
}
else
{
// let url = URL(string: videoUrl) {
Text
(
msg
)
// isVideoPlayerPresented = true
.
font
(
FontStyle
.
DefaultText
.
font
)
// }
.
foregroundColor
(
ColorSet
.
BodyChat
.
color
)
// }
.
padding
(
15
)
}
else
{
.
background
(
ColorSet
.
ChatBaloon
.
color
)
Text
(
msg
)
.
border
(
Color
.
red
.
gradient
.
opacity
(
0.8
),
width
:
(
message
.
mode
==
ChatMode
.
warningProgress
.
rawValue
?
2
:
0
))
.
font
(
FontStyle
.
DefaultText
.
font
)
.
clipShape
(
.
foregroundColor
(
ColorSet
.
BodyChat
.
color
)
.
rect
(
.
padding
(
15
)
topLeadingRadius
:
10
,
.
background
(
ColorSet
.
ChatBaloon
.
color
)
bottomLeadingRadius
:
0
,
.
border
(
Color
.
red
.
gradient
.
opacity
(
0.8
),
width
:
(
message
.
mode
==
ChatMode
.
warningProgress
.
rawValue
?
2
:
0
))
bottomTrailingRadius
:
10
,
.
clipShape
(
topTrailingRadius
:
10
.
rect
(
))
topLeadingRadius
:
10
,
}
bottomLeadingRadius
:
0
,
bottomTrailingRadius
:
10
,
topTrailingRadius
:
10
))
}
}
}
}
}
HStack
(
spacing
:
5
){
HStack
(
spacing
:
5
){
//MARK: - 既読マーク
//MARK: - 既読マーク
Text
(
DateTextLib
.
ISO86012FormatText
(
message
.
time
,
format
:
"yyyy/MM/dd HH:mm"
,
errFormat
:
""
))
Text
(
DateTextLib
.
ISO86012FormatText
(
message
.
time
,
format
:
"yyyy/MM/dd HH:mm"
,
errFormat
:
""
))
.
padding
(
.
trailing
,
8
)
.
padding
(
.
trailing
,
8
)
let
viewerCnt
=
viewerCnt
()
let
viewerCnt
=
viewerCnt
()
Image
(
"chat_company"
)
Image
(
"chat_company"
)
.
resizable
()
.
resizable
()
.
scaledToFit
()
.
scaledToFit
()
.
frame
(
width
:
12
,
height
:
12
)
.
frame
(
width
:
12
,
height
:
12
)
.
padding
(
.
trailing
,
4
)
.
padding
(
.
trailing
,
4
)
Text
(
String
(
viewerCnt
.
1
))
Text
(
String
(
viewerCnt
.
1
))
Image
(
"chat_ship"
)
Image
(
"chat_ship"
)
.
resizable
()
.
resizable
()
.
scaledToFit
()
.
scaledToFit
()
.
frame
(
width
:
12
,
height
:
12
)
.
frame
(
width
:
12
,
height
:
12
)
.
padding
(
.
trailing
,
4
)
.
padding
(
.
trailing
,
4
)
Text
(
String
(
viewerCnt
.
0
))
Text
(
String
(
viewerCnt
.
0
))
}
.
font
(
FontStyle
.
DateText
.
font
)
.
foregroundColor
(
ColorSet
.
ChatDate
.
color
)
}
}
.
padding
(
.
trailing
,
20
)
.
font
(
FontStyle
.
DateText
.
font
)
.
foregroundColor
(
ColorSet
.
ChatDate
.
color
)
}
.
padding
(
.
trailing
,
20
)
}
}
// .fullScreenCover(isPresented: $isVideoPlayerPresented) {
// if let videoUrl = message.videoUrl,
// let url = URL(string: videoUrl) {
// VideoPlayerView(videoURL: url)
// }
// }
}
}
func
viewerCnt
()
->
(
Int
,
Int
)
{
func
viewerCnt
()
->
(
Int
,
Int
)
{
...
@@ -83,6 +95,16 @@ struct MyChatContentView: View {
...
@@ -83,6 +95,16 @@ struct MyChatContentView: View {
}
}
}
}
//struct VideoPlayerView: View {
// let videoURL: URL
//
// var body: some View {
// VideoPlayer(player: AVPlayer(url: videoURL))
// .navigationTitle("動画再生")
// .navigationBarTitleDisplayMode(.inline)
// }
//}
#Preview {
#Preview {
MyChatContentView
(
message
:
ChatMessage
(
MyChatContentView
(
message
:
ChatMessage
(
shipId
:
10000003
,
shipId
:
10000003
,
...
...
Seilassist/Sailassist/Chat/View/OtherChatContentView.swift
View file @
65b05c9e
...
@@ -4,10 +4,11 @@
...
@@ -4,10 +4,11 @@
//
//
// Created by Mamoru Sugita on 2023/11/09.
// Created by Mamoru Sugita on 2023/11/09.
//
//
import
SwiftUI
import
SwiftUI
import
AVKit
struct
OtherChatContentView
:
View
{
struct
OtherChatContentView
:
View
{
@State
private
var
isVideoPlayerPresented
=
false
var
message
:
ChatMessage
var
message
:
ChatMessage
var
onMediaLoaded
:
(()
->
Void
)?
=
nil
var
onMediaLoaded
:
(()
->
Void
)?
=
nil
...
@@ -24,8 +25,13 @@ struct OtherChatContentView: View {
...
@@ -24,8 +25,13 @@ struct OtherChatContentView: View {
if
let
msg
=
message
.
message
{
if
let
msg
=
message
.
message
{
if
msg
.
contains
(
".jpg"
)
||
msg
.
contains
(
".png"
)
{
if
msg
.
contains
(
".jpg"
)
||
msg
.
contains
(
".png"
)
{
ChatUrlImageView
(
imageUrl
:
msg
,
onLoad
:
onMediaLoaded
)
ChatUrlImageView
(
imageUrl
:
msg
,
onLoad
:
onMediaLoaded
)
}
else
if
msg
.
contains
(
".mp4"
)
||
msg
.
contains
(
".mov"
)
{
// .onTapGesture {
ChatUrlVideoView
(
imageUrl
:
msg
,
onLoad
:
onMediaLoaded
)
// if message.type == 3,
// let videoUrl = message.videoUrl,
// let url = URL(string: videoUrl) {
// isVideoPlayerPresented = true
// }
// }
}
else
{
}
else
{
Text
(
msg
)
Text
(
msg
)
.
font
(
FontStyle
.
DefaultText
.
font
)
.
font
(
FontStyle
.
DefaultText
.
font
)
...
@@ -54,6 +60,12 @@ struct OtherChatContentView: View {
...
@@ -54,6 +60,12 @@ struct OtherChatContentView: View {
.
padding
(
.
leading
,
20
)
.
padding
(
.
leading
,
20
)
Spacer
()
Spacer
()
}
}
// .fullScreenCover(isPresented: $isVideoPlayerPresented) {
// if let videoUrl = message.videoUrl,
// let url = URL(string: videoUrl) {
// VideoPlayerView(videoURL: url)
// }
// }
}
}
}
}
...
...
Seilassist/Sailassist/Chat/ViewModel/ChatInputViewModel.swift
View file @
65b05c9e
import
SwiftUI
import
SwiftUI
import
Combine
import
Combine
import
Speech
import
Speech
import
AVFoundation
import
UIKit
enum
MediaInputType
{
enum
MediaInputType
{
case
none
case
none
...
@@ -38,30 +40,6 @@ class ChatInputViewModel: ObservableObject {
...
@@ -38,30 +40,6 @@ class ChatInputViewModel: ObservableObject {
}
}
}
}
// MARK: - Send Chat Image
func
sendChatImage
(
_
uploadImage
:
ReqUploadImage
)
{
Task
{
do
{
isUploadingDialogPresented
=
true
let
response
=
try
await
sessionUploadImage
.
requestUploadImage
(
uploadImage
)
print
(
debug
:
"Upload success:
\(
response
)
"
)
let
serverSession
=
ServerSession
()
_
=
serverSession
.
fromJSON
(
resultData
:
response
,
resltType
:
ResLogin
.
self
)
sessionUploadImage
.
progress
=
0.0
// 完了後リセット
isUploadingDialogPresented
=
false
}
catch
{
print
(
debug
:
"Upload failed:
\(
error
)
"
)
sessionUploadImage
.
progress
=
0.0
// エラー時もリセット
isUploadingDialogPresented
=
false
failedUploadImage
=
uploadImage
isRetryDialogPresented
=
true
}
}
}
// MARK: - Send Chat Message
// MARK: - Send Chat Message
func
sendChatMessage
()
{
func
sendChatMessage
()
{
guard
!
SharingData
.
message
.
sendInf
else
{
return
}
guard
!
SharingData
.
message
.
sendInf
else
{
return
}
...
@@ -109,14 +87,62 @@ class ChatInputViewModel: ObservableObject {
...
@@ -109,14 +87,62 @@ class ChatInputViewModel: ObservableObject {
textViewHeight
=
max
(
newSize
.
height
,
40
)
// 最低高さを40に設定
textViewHeight
=
max
(
newSize
.
height
,
40
)
// 最低高さを40に設定
}
}
// MARK: - ローカル画像を一時保存してURLを取得
// MARK: - Send Chat Image
func
sendChatImage
(
_
uploadImage
:
ReqUploadImage
)
{
Task
{
do
{
isUploadingDialogPresented
=
true
let
response
=
try
await
sessionUploadImage
.
requestUploadImage
(
uploadImage
)
print
(
debug
:
"Upload success:
\(
response
)
"
)
let
serverSession
=
ServerSession
()
_
=
serverSession
.
fromJSON
(
resultData
:
response
,
resltType
:
ResLogin
.
self
)
sessionUploadImage
.
progress
=
0.0
// 完了後リセット
isUploadingDialogPresented
=
false
}
catch
{
print
(
debug
:
"Upload failed:
\(
error
)
"
)
sessionUploadImage
.
progress
=
0.0
// エラー時もリセット
isUploadingDialogPresented
=
false
failedUploadImage
=
uploadImage
isRetryDialogPresented
=
true
}
}
}
//MARK: - Send Chat Video
func
sendChatVideo
(
_
uploadVideo
:
ReqUploadImage
)
{
Task
{
do
{
isUploadingDialogPresented
=
true
let
response
=
try
await
SessionUploadImage
.
OnlyOne
.
requestUploadVideo
(
uploadVideo
)
print
(
debug
:
"Video upload success:
\(
response
)
"
)
let
serverSession
=
ServerSession
()
_
=
serverSession
.
fromJSON
(
resultData
:
response
,
resltType
:
ResLogin
.
self
)
SessionUploadImage
.
OnlyOne
.
progress
=
0.0
isUploadingDialogPresented
=
false
}
catch
{
print
(
debug
:
"Video upload failed:
\(
error
)
"
)
SessionUploadImage
.
OnlyOne
.
progress
=
0.0
isUploadingDialogPresented
=
false
failedUploadImage
=
uploadVideo
isRetryDialogPresented
=
true
}
}
}
// MARK: - ローカル画像を一時保存してURLを取得 (静止画)
func
saveImageToTemporaryDirectory
(
_
image
:
UIImage
)
->
String
?
{
func
saveImageToTemporaryDirectory
(
_
image
:
UIImage
)
->
String
?
{
let
fileName
=
UUID
()
.
uuidString
+
".jpg"
let
fileName
=
UUID
()
.
uuidString
+
".jpg"
let
tempDir
=
FileManager
.
default
.
temporaryDirectory
let
tempDir
=
FileManager
.
default
.
temporaryDirectory
let
fileURL
=
tempDir
.
appendingPathComponent
(
fileName
)
let
fileURL
=
tempDir
.
appendingPathComponent
(
fileName
)
guard
let
data
=
image
.
jpegData
(
compressionQuality
:
0.8
)
else
{
guard
let
data
=
image
.
jpegData
(
compressionQuality
:
0.8
)
else
{
print
(
"Image JPEG conversion failed"
)
print
(
debug
:
"Image JPEG conversion failed"
)
return
nil
return
nil
}
}
...
@@ -124,7 +150,7 @@ class ChatInputViewModel: ObservableObject {
...
@@ -124,7 +150,7 @@ class ChatInputViewModel: ObservableObject {
try
data
.
write
(
to
:
fileURL
)
try
data
.
write
(
to
:
fileURL
)
return
fileURL
.
absoluteString
return
fileURL
.
absoluteString
}
catch
{
}
catch
{
print
(
"Image JPEG conversion failed:
\(
error
.
localizedDescription
)
"
)
print
(
debug
:
"Image JPEG conversion failed:
\(
error
.
localizedDescription
)
"
)
return
nil
return
nil
}
}
}
}
...
@@ -167,4 +193,75 @@ class ChatInputViewModel: ObservableObject {
...
@@ -167,4 +193,75 @@ class ChatInputViewModel: ObservableObject {
sendChatImage
(
uploadImage
)
sendChatImage
(
uploadImage
)
}
}
}
}
// MARK: - ローカル画像を一時保存してURLを取得 (動画)
func
saveVideoToTemporaryDirectory
(
_
data
:
Data
)
->
URL
?
{
let
fileName
=
UUID
()
.
uuidString
+
".mp4"
let
tempDir
=
FileManager
.
default
.
temporaryDirectory
let
fileURL
=
tempDir
.
appendingPathComponent
(
fileName
)
do
{
try
data
.
write
(
to
:
fileURL
)
return
fileURL
}
catch
{
print
(
debug
:
"Video save failed:
\(
error
.
localizedDescription
)
"
)
return
nil
}
}
//MARK: - 動画データからサムネイル画像を生成する
func
generateThumbnail
(
from
videoURL
:
URL
,
at
time
:
CMTime
=
CMTime
(
seconds
:
1.0
,
preferredTimescale
:
600
))
->
UIImage
?
{
let
asset
=
AVAsset
(
url
:
videoURL
)
let
imageGenerator
=
AVAssetImageGenerator
(
asset
:
asset
)
imageGenerator
.
appliesPreferredTrackTransform
=
true
do
{
let
cgImage
=
try
imageGenerator
.
copyCGImage
(
at
:
time
,
actualTime
:
nil
)
return
UIImage
(
cgImage
:
cgImage
)
}
catch
{
print
(
"サムネイル生成失敗:
\(
error
.
localizedDescription
)
"
)
return
nil
}
}
//MARK: - 送信前に送信動画をテンポラリに入れる
func
sendVideoToTemporary
(
_
data
:
Data
)
{
if
let
videoURL
=
saveVideoToTemporaryDirectory
(
data
),
let
image
=
generateThumbnail
(
from
:
videoURL
),
let
localURL
=
saveImageToTemporaryDirectory
(
image
)
{
tempId
=
UUID
()
.
uuidString
let
viewer
=
Viewer
(
time
:
DateTextLib
.
Date2ISO8601Text
(
Date
()),
location
:
2
,
id
:
""
,
name
:
""
)
let
tempMessage
=
ChatMessage
(
shipId
:
Preferences
.
shipId
,
messageId
:
tempId
,
type
:
3
,
// 動画タイプ
time
:
DateTextLib
.
Date2ISO8601Text
(
Date
()),
location
:
2
,
from
:
Preferences
.
UserName
,
fromId
:
String
(
SharingData
.
my
.
id
),
mode
:
SharingData
.
message
.
mode
?
1
:
0
,
message
:
localURL
,
// videoUrl: videoURL.absoluteString,
stampId
:
0
,
viewer
:
[
viewer
]
)
SharingData
.
message
.
messages
.
append
(
tempMessage
)
let
uploadVideo
=
ReqUploadImage
(
shipId
:
Preferences
.
shipId
,
messageId
:
tempId
,
location
:
2
,
from
:
Preferences
.
UserName
,
fromId
:
String
(
SharingData
.
my
.
id
),
files
:
data
)
sendChatVideo
(
uploadVideo
)
}
}
}
}
Seilassist/Sailassist/Json/ResGetMessages.swift
View file @
65b05c9e
...
@@ -28,7 +28,10 @@ struct ChatMessage: Codable {
...
@@ -28,7 +28,10 @@ struct ChatMessage: Codable {
var
fromId
:
String
?
//ユーザーID
var
fromId
:
String
?
//ユーザーID
var
mode
:
Int
// 0:通常 , 1:Warning中
var
mode
:
Int
// 0:通常 , 1:Warning中
var
message
:
String
?
//テキスト時:テキスト , 画像時:サムネイルのUri
var
message
:
String
?
//テキスト時:テキスト , 画像時:サムネイルのUri
// var videoUrl: String? //TODO: - テスト
var
stampId
:
Int
//スタンプ番号 0:Fire~
var
stampId
:
Int
//スタンプ番号 0:Fire~
// var Latitude: Double? //TODO: - テスト
// var Longitude: Double? //TODO: - テスト
var
viewer
:
[
Viewer
]
=
[]
//閲覧者情報
var
viewer
:
[
Viewer
]
=
[]
//閲覧者情報
}
}
...
...
Seilassist/Sailassist/SailassistApp.swift
View file @
65b05c9e
...
@@ -218,7 +218,6 @@ class AppDelegate: NSObject, UIApplicationDelegate ,MSNotificationHubDelegate, M
...
@@ -218,7 +218,6 @@ class AppDelegate: NSObject, UIApplicationDelegate ,MSNotificationHubDelegate, M
if
existingMsg
.
message
!.
hasPrefix
(
"file://"
)
{
if
existingMsg
.
message
!.
hasPrefix
(
"file://"
)
{
updatedMsg
.
message
=
existingMsg
.
message
updatedMsg
.
message
=
existingMsg
.
message
}
}
self
.
msg
.
messages
[
index
]
=
updatedMsg
self
.
msg
.
messages
[
index
]
=
updatedMsg
}
else
{
}
else
{
self
.
msg
.
messages
.
append
(
newMsg
)
self
.
msg
.
messages
.
append
(
newMsg
)
...
...
Seilassist/Sailassist/ServerSession/SessionUploadImage.swift
View file @
65b05c9e
...
@@ -28,9 +28,7 @@ class SessionUploadImage : ObservableObject {
...
@@ -28,9 +28,7 @@ class SessionUploadImage : ObservableObject {
//MARK: - 画像のアップロード
//MARK: - 画像のアップロード
func
requestUploadImage
(
_
uploadImage
:
ReqUploadImage
)
async
throws
->
Data
{
func
requestUploadImage
(
_
uploadImage
:
ReqUploadImage
)
async
throws
->
Data
{
print
(
debug
:
"called"
)
print
(
debug
:
"called"
)
guard
!
Calling
else
{
guard
!
Calling
else
{
throw
APIError
.
busy
}
throw
APIError
.
busy
}
Calling
=
true
Calling
=
true
defer
{
Calling
=
false
}
defer
{
Calling
=
false
}
...
@@ -92,6 +90,74 @@ class SessionUploadImage : ObservableObject {
...
@@ -92,6 +90,74 @@ class SessionUploadImage : ObservableObject {
}
}
}
}
// MARK: - 動画のアップロード
func
requestUploadVideo
(
_
uploadVideo
:
ReqUploadImage
)
async
throws
->
Data
{
print
(
"Video upload called"
)
guard
!
Calling
else
{
throw
APIError
.
busy
}
Calling
=
true
defer
{
Calling
=
false
}
guard
let
req_url
=
URL
(
string
:
HttpRequestType
.
UploadImage
.
rawValue
)
else
{
throw
APIError
.
invalidURL
}
let
boundary
=
"----------
\(
UUID
()
.
uuidString
)
"
var
httpBody
=
Data
()
func
appendFormField
(
name
:
String
,
value
:
String
)
{
httpBody
.
append
(
"--
\(
boundary
)\r\n
"
.
data
(
using
:
.
utf8
)
!
)
httpBody
.
append
(
"Content-Disposition: form-data; name=
\"\(
name
)\"\r\n\r\n
"
.
data
(
using
:
.
utf8
)
!
)
httpBody
.
append
(
"
\(
value
)\r\n
"
.
data
(
using
:
.
utf8
)
!
)
}
appendFormField
(
name
:
"ShipId"
,
value
:
String
(
uploadVideo
.
shipId
))
appendFormField
(
name
:
"MessageId"
,
value
:
uploadVideo
.
messageId
)
appendFormField
(
name
:
"Location"
,
value
:
String
(
uploadVideo
.
location
))
appendFormField
(
name
:
"From"
,
value
:
uploadVideo
.
from
)
appendFormField
(
name
:
"FromId"
,
value
:
uploadVideo
.
fromId
)
httpBody
.
append
(
"--
\(
boundary
)\r\n
"
.
data
(
using
:
.
utf8
)
!
)
httpBody
.
append
(
"Content-Disposition: form-data; name=
\"
files
\"
; filename=
\"
itemp.mp4
\"\r\n
"
.
data
(
using
:
.
utf8
)
!
)
httpBody
.
append
(
"Content-Type: video/mp4
\r\n\r\n
"
.
data
(
using
:
.
utf8
)
!
)
httpBody
.
append
(
uploadVideo
.
files
)
httpBody
.
append
(
"
\r\n
--
\(
boundary
)
--
\r\n
"
.
data
(
using
:
.
utf8
)
!
)
var
request
=
URLRequest
(
url
:
req_url
)
request
.
httpMethod
=
"POST"
request
.
setValue
(
"multipart/form-data; boundary=
\(
boundary
)
"
,
forHTTPHeaderField
:
"Content-Type"
)
let
session
=
URLSession
(
configuration
:
.
default
,
delegate
:
UploadProgressDelegate
(
parent
:
self
),
delegateQueue
:
nil
)
isUploading
=
true
return
try
await
withCheckedThrowingContinuation
{
continuation
in
self
.
uploadTask
=
session
.
uploadTask
(
with
:
request
,
from
:
httpBody
)
{
data
,
response
,
error
in
DispatchQueue
.
main
.
async
{
self
.
isUploading
=
false
self
.
progress
=
0.0
}
if
let
error
=
error
{
continuation
.
resume
(
throwing
:
error
)
return
}
guard
let
httpResponse
=
response
as?
HTTPURLResponse
,
(
200
...
299
)
.
contains
(
httpResponse
.
statusCode
),
let
data
=
data
else
{
continuation
.
resume
(
throwing
:
APIError
.
serverError
)
return
}
continuation
.
resume
(
returning
:
data
)
}
self
.
uploadTask
?
.
resume
()
}
}
func
postFormAsync
(
boundary
:
String
,
url
:
URL
,
body
:
Data
)
async
throws
->
Data
{
func
postFormAsync
(
boundary
:
String
,
url
:
URL
,
body
:
Data
)
async
throws
->
Data
{
var
request
=
URLRequest
(
url
:
url
)
var
request
=
URLRequest
(
url
:
url
)
request
.
httpMethod
=
"POST"
request
.
httpMethod
=
"POST"
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment