@@ -111,32 +111,17 @@ struct ComposeScreen: View {
111111 ScrollView ( . horizontal) {
112112 HStack {
113113 ForEach ( viewModel. mediaViewModel. items) { item in
114- if let image = item. image {
115- Image ( uiImage: image)
116- . resizable ( )
117- . scaledToFill ( )
118- . frame ( width: 128 , height: 128 )
119- . cornerRadius ( 8 )
120- . contextMenu {
121- Button ( action: {
122- withAnimation {
123- viewModel. mediaViewModel. remove ( item: item)
124- }
125- } , label: {
126- Label {
127- Text ( " delete " )
128- } icon: {
129- Image ( " fa-trash " )
130- }
131- } )
132- }
133- }
114+ ComposeMediaItemView ( item: item, mediaViewModel: viewModel. mediaViewModel)
134115 }
135116 }
136117 }
137- Toggle ( isOn: $viewModel. mediaViewModel. sensitive, label: {
138- Text ( " compose_media_mark_sensitive " )
139- } )
118+ StateView ( state: presenter. state. composeConfig) { config in
119+ if let media = config. media, media. canSensitive {
120+ Toggle ( isOn: $viewModel. mediaViewModel. sensitive, label: {
121+ Text ( " compose_media_mark_sensitive " )
122+ } )
123+ }
124+ }
140125 }
141126 if viewModel. pollViewModel. enabled {
142127 HStack (
@@ -214,13 +199,16 @@ struct ComposeScreen: View {
214199 ) {
215200 if !viewModel. pollViewModel. enabled {
216201 StateView ( state: presenter. state. composeConfig) { config in
217- if let media = config. media {
218- PhotosPicker ( selection: Binding ( get: {
219- viewModel. mediaViewModel. selectedItems
220- } , set: { value in
221- viewModel. mediaViewModel. selectedItems = value
222- viewModel. mediaViewModel. update ( )
223- } ) , matching: . any( of: [ . images, . videos, . livePhotos] ) ) {
202+ if config. media != nil {
203+ PhotosPicker (
204+ selection: Binding ( get: {
205+ viewModel. mediaViewModel. selectedItems
206+ } , set: { value in
207+ viewModel. mediaViewModel. selectedItems = value
208+ viewModel. mediaViewModel. update ( )
209+ } ) ,
210+ maxSelectionCount: viewModel. mediaViewModel. maxSize,
211+ matching: . any( of: [ . images, . videos, . livePhotos] ) ) {
224212 Image ( " fa-image " )
225213 }
226214 }
@@ -303,11 +291,54 @@ struct ComposeScreen: View {
303291 }
304292 }
305293 }
294+ StateView ( state: presenter. state. composeConfig) { config in
295+ if let languageConfig = config. language {
296+ Menu {
297+ ForEach ( languageConfig. sortedIsoCodes, id: \. self) { code in
298+ Button {
299+ if languageConfig. maxCount > 1 {
300+ if viewModel. languages. contains ( code) {
301+ if viewModel. languages. count > 1 {
302+ viewModel. languages. removeAll { $0 == code }
303+ }
304+ } else {
305+ if viewModel. languages. count < languageConfig. maxCount {
306+ viewModel. languages. append ( code)
307+ }
308+ }
309+ } else {
310+ viewModel. languages = [ code]
311+ }
312+ } label: {
313+ HStack {
314+ Text ( Locale . current. localizedString ( forLanguageCode: code) ?? code)
315+ if viewModel. languages. contains ( code) {
316+ Image ( systemName: " checkmark " )
317+ }
318+ }
319+ }
320+ }
321+ } label: {
322+ if let first = viewModel. languages. first, viewModel. languages. count == 1 {
323+ Text ( first. uppercased ( ) )
324+ . font ( . caption)
325+ . bold ( )
326+ . padding ( 4 )
327+ . overlay (
328+ RoundedRectangle ( cornerRadius: 4 )
329+ . stroke ( Color . primary, lineWidth: 1 )
330+ )
331+ } else {
332+ Image ( systemName: " globe " )
333+ }
334+ }
335+ }
336+ }
306337 }
307- . scrollIndicators ( . hidden)
308338 . font ( . title)
309339 . buttonStyle ( . plain)
310340 }
341+ . scrollIndicators ( . hidden)
311342 Spacer ( )
312343 StateView ( state: presenter. state. composeConfig) { config in
313344 if let maxLength = config. text? . maxLength {
@@ -333,8 +364,16 @@ struct ComposeScreen: View {
333364 . onSuccessOf ( of: presenter. state. composeConfig) { config in
334365 if let media = config. media {
335366 viewModel. mediaViewModel. maxSize = Int ( media. maxCount)
367+ viewModel. mediaViewModel. enableAltText = media. altTextMaxLength > 0
368+ viewModel. mediaViewModel. altTextMaxLength = Int ( media. altTextMaxLength)
336369 }
337370 }
371+ . onChange ( of: viewModel. text) { oldValue, newValue in
372+ presenter. state. setText ( value: newValue)
373+ }
374+ . onChange ( of: viewModel. mediaViewModel. items. count) { _, newValue in
375+ presenter. state. setMediaSize ( value: Int32 ( newValue) )
376+ }
338377 . toolbarTitleDisplayMode ( . inline)
339378 . toolbar {
340379 ToolbarItem ( placement: . principal) {
@@ -368,7 +407,7 @@ struct ComposeScreen: View {
368407 Image ( systemName: " paperplane.fill " )
369408 }
370409 }
371- . disabled ( viewModel . text . isEmpty )
410+ . disabled ( !presenter . state . canSend )
372411 }
373412 }
374413 }
@@ -407,7 +446,7 @@ struct ComposeScreen: View {
407446 account: account,
408447 content: viewModel. text,
409448 visibility: getVisibility ( ) ,
410- language: [ " en " ] ,
449+ language: viewModel . languages ,
411450 medias: getMedia ( ) ,
412451 sensitive: viewModel. mediaViewModel. sensitive,
413452 spoilerText: viewModel. contentWarning,
@@ -426,9 +465,9 @@ struct ComposeScreen: View {
426465 dismiss ( )
427466 }
428467
429- private func getMedia( ) -> [ FileItem ] {
468+ private func getMedia( ) -> [ ComposeData . Media ] {
430469 return viewModel. mediaViewModel. items. map { item in
431- FileItem ( name: item. item. itemIdentifier, data: KotlinByteArray . from ( data: item. data!) )
470+ . init ( file : . init ( name: item. item. itemIdentifier, data: KotlinByteArray . from ( data: item. data!) ) , altText : item . altText . isEmpty ? nil : item . altText )
432471 }
433472 }
434473 private func getReferenceStatus( ) -> ComposeData . ReferenceStatus ? {
@@ -460,6 +499,12 @@ class ComposeInputViewModel {
460499 var pollViewModel = PollViewModel ( )
461500 var mediaViewModel = MediaViewModel ( )
462501 var visibility : UiTimeline . ItemContentStatusTopEndContentVisibilityType = . public
502+ var languages : [ String ] = {
503+ if let code = Locale . current. language. languageCode? . identifier {
504+ return [ code]
505+ }
506+ return [ " en " ]
507+ } ( )
463508
464509
465510 func showEmojiPanel( ) {
@@ -488,6 +533,8 @@ class MediaViewModel {
488533 var items : [ MediaItem ] = [ ]
489534 var sensitive = false
490535 var maxSize = 4
536+ var enableAltText = true
537+ var altTextMaxLength = 500
491538 func update( ) {
492539 if selectedItems. count > maxSize {
493540 selectedItems = Array ( selectedItems [ ( selectedItems. count - 4 ) ... ( selectedItems. count - 1 ) ] )
@@ -514,11 +561,12 @@ class MediaItem: Equatable, Identifiable {
514561 let item : PhotosPickerItem
515562 var image : UIImage ?
516563 var data : Data ?
517- var id : String {
518- item . itemIdentifier ?? UUID ( ) . uuidString
519- }
564+ var altText : String = " "
565+ let id : String
566+
520567 init ( item: PhotosPickerItem ) {
521568 self . item = item
569+ self . id = item. itemIdentifier ?? UUID ( ) . uuidString
522570 item. loadTransferable ( type: Data . self) { result in
523571 do {
524572 if let data = try result. get ( ) {
@@ -614,3 +662,61 @@ extension ComposeScreen {
614662 self . _presenter = . init( wrappedValue: . init( presenter: ComposePresenter ( accountType: accountType, status: composeStatus) ) )
615663 }
616664}
665+
666+ struct ComposeMediaItemView : View {
667+ let item : MediaItem
668+ var mediaViewModel : MediaViewModel
669+ @State private var showAltTextEditor = false
670+
671+ var body : some View {
672+ if let image = item. image {
673+ Image ( uiImage: image)
674+ . resizable ( )
675+ . scaledToFill ( )
676+ . frame ( width: 128 , height: 128 )
677+ . cornerRadius ( 8 )
678+ . overlay ( alignment: . bottomLeading) {
679+ if mediaViewModel. enableAltText && !item. altText. isEmpty {
680+ Text ( " ALT " )
681+ . font ( . caption2)
682+ . bold ( )
683+ . foregroundStyle ( . black)
684+ . padding ( . horizontal, 4 )
685+ . padding ( . vertical, 2 )
686+ . background ( . white. opacity ( 0.8 ) )
687+ . clipShape ( RoundedRectangle ( cornerRadius: 4 ) )
688+ . padding ( 4 )
689+ }
690+ }
691+ . onTapGesture {
692+ if mediaViewModel. enableAltText {
693+ showAltTextEditor = true
694+ }
695+ }
696+ . contextMenu {
697+ Button ( action: {
698+ withAnimation {
699+ mediaViewModel. remove ( item: item)
700+ }
701+ } , label: {
702+ Label {
703+ Text ( " delete " )
704+ } icon: {
705+ Image ( " fa-trash " )
706+ }
707+ } )
708+
709+ if mediaViewModel. enableAltText {
710+ Button {
711+ showAltTextEditor = true
712+ } label: {
713+ Label ( " Edit Description " , systemImage: " pencil " )
714+ }
715+ }
716+ }
717+ . sheet ( isPresented: $showAltTextEditor) {
718+ AltTextEditSheet ( item: item, maxLength: mediaViewModel. altTextMaxLength)
719+ }
720+ }
721+ }
722+ }
0 commit comments