diff options
Diffstat (limited to 'src/ios.m')
-rw-r--r-- | src/ios.m | 158 |
1 files changed, 144 insertions, 14 deletions
@@ -22,6 +22,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
22 | 22 | ||
23 | #include "ios.h" | 23 | #include "ios.h" |
24 | #include "app.h" | 24 | #include "app.h" |
25 | #include "audio/player.h" | ||
25 | #include "ui/command.h" | 26 | #include "ui/command.h" |
26 | #include "ui/window.h" | 27 | #include "ui/window.h" |
27 | 28 | ||
@@ -32,9 +33,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
32 | #include <SDL_syswm.h> | 33 | #include <SDL_syswm.h> |
33 | #include <SDL_timer.h> | 34 | #include <SDL_timer.h> |
34 | 35 | ||
35 | #import <UIKit/UIKit.h> | ||
36 | #import <CoreHaptics/CoreHaptics.h> | ||
37 | #import <AVFAudio/AVFAudio.h> | 36 | #import <AVFAudio/AVFAudio.h> |
37 | #import <CoreHaptics/CoreHaptics.h> | ||
38 | #import <UIKit/UIKit.h> | ||
39 | #import <MediaPlayer/MediaPlayer.h> | ||
38 | 40 | ||
39 | static iBool isSystemDarkMode_ = iFalse; | 41 | static iBool isSystemDarkMode_ = iFalse; |
40 | static iBool isPhone_ = iFalse; | 42 | static iBool isPhone_ = iFalse; |
@@ -64,6 +66,7 @@ API_AVAILABLE(ios(13.0)) | |||
64 | @interface HapticState : NSObject | 66 | @interface HapticState : NSObject |
65 | @property (nonatomic, strong) CHHapticEngine *engine; | 67 | @property (nonatomic, strong) CHHapticEngine *engine; |
66 | @property (nonatomic, strong) NSDictionary *tapDef; | 68 | @property (nonatomic, strong) NSDictionary *tapDef; |
69 | @property (nonatomic, strong) NSDictionary *gentleTapDef; | ||
67 | @end | 70 | @end |
68 | 71 | ||
69 | @implementation HapticState | 72 | @implementation HapticState |
@@ -105,26 +108,47 @@ API_AVAILABLE(ios(13.0)) | |||
105 | CHHapticPatternKeyEvent: @{ | 108 | CHHapticPatternKeyEvent: @{ |
106 | CHHapticPatternKeyEventType: CHHapticEventTypeHapticTransient, | 109 | CHHapticPatternKeyEventType: CHHapticEventTypeHapticTransient, |
107 | CHHapticPatternKeyTime: @0.0, | 110 | CHHapticPatternKeyTime: @0.0, |
108 | CHHapticPatternKeyEventDuration:@0.1 | 111 | CHHapticPatternKeyEventDuration:@0.1, |
112 | CHHapticPatternKeyEventParameters: @[ | ||
113 | @{ | ||
114 | CHHapticPatternKeyParameterID: CHHapticEventParameterIDHapticIntensity, | ||
115 | CHHapticPatternKeyParameterValue: @1.0 | ||
116 | } | ||
117 | ] | ||
118 | }, | ||
119 | }, | ||
120 | ] | ||
121 | }; | ||
122 | self.gentleTapDef = @{ | ||
123 | CHHapticPatternKeyPattern: | ||
124 | @[ | ||
125 | @{ | ||
126 | CHHapticPatternKeyEvent: @{ | ||
127 | CHHapticPatternKeyEventType: CHHapticEventTypeHapticTransient, | ||
128 | CHHapticPatternKeyTime: @0.0, | ||
129 | CHHapticPatternKeyEventDuration:@0.1, | ||
130 | CHHapticPatternKeyEventParameters: @[ | ||
131 | @{ | ||
132 | CHHapticPatternKeyParameterID: CHHapticEventParameterIDHapticIntensity, | ||
133 | CHHapticPatternKeyParameterValue: @0.33 | ||
134 | } | ||
135 | ] | ||
109 | }, | 136 | }, |
110 | }, | 137 | }, |
111 | ], | 138 | ] |
112 | }; | 139 | }; |
113 | } | 140 | } |
114 | 141 | ||
115 | -(void)playTapEffect { | 142 | |
143 | -(void)playHapticEffect:(NSDictionary *)def { | ||
116 | NSError *error = nil; | 144 | NSError *error = nil; |
117 | CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithDictionary:self.tapDef | 145 | CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithDictionary:def |
118 | error:&error]; | 146 | error:&error]; |
119 | // TODO: Check the error. | 147 | // TODO: Check the error. |
120 | id<CHHapticPatternPlayer> player = [self.engine createPlayerWithPattern:pattern error:&error]; | 148 | id<CHHapticPatternPlayer> player = [self.engine createPlayerWithPattern:pattern error:&error]; |
121 | // TODO: Check the error. | 149 | // TODO: Check the error. |
122 | [self.engine startWithCompletionHandler:^(NSError *err){ | 150 | [self.engine startWithCompletionHandler:^(NSError *err){ |
123 | if (err == nil) { | 151 | if (err == nil) { |
124 | /* Just keep it running. */ | ||
125 | // [self.engine notifyWhenPlayersFinished:^(NSError * _Nullable error) { | ||
126 | // return CHHapticEngineFinishedActionStopEngine; | ||
127 | // }]; | ||
128 | NSError *startError = nil; | 152 | NSError *startError = nil; |
129 | [player startAtTime:0.0 error:&startError]; | 153 | [player startAtTime:0.0 error:&startError]; |
130 | } | 154 | } |
@@ -181,11 +205,22 @@ static AppState *appState_; | |||
181 | 205 | ||
182 | - (void)documentPicker:(UIDocumentPickerViewController *)controller | 206 | - (void)documentPicker:(UIDocumentPickerViewController *)controller |
183 | didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls { | 207 | didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls { |
184 | [self removeSavedFile]; | 208 | if (fileBeingSaved) { |
209 | [self removeSavedFile]; | ||
210 | } | ||
211 | else { | ||
212 | /* A file is being opened. */ | ||
213 | NSURL *url = [urls firstObject]; | ||
214 | iString *path = localFilePathFromUrl_String(collectNewCStr_String([[url absoluteString] | ||
215 | UTF8String])); | ||
216 | postCommandf_App("file.open temp:1 path:%s", cstrCollect_String(path)); | ||
217 | } | ||
185 | } | 218 | } |
186 | 219 | ||
187 | - (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller { | 220 | - (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller { |
188 | [self removeSavedFile]; | 221 | if (fileBeingSaved) { |
222 | [self removeSavedFile]; | ||
223 | } | ||
189 | } | 224 | } |
190 | 225 | ||
191 | -(void)keyboardOnScreen:(NSNotification *)notification { | 226 | -(void)keyboardOnScreen:(NSNotification *)notification { |
@@ -230,6 +265,55 @@ void setupApplication_iOS(void) { | |||
230 | name:UIKeyboardWillHideNotification | 265 | name:UIKeyboardWillHideNotification |
231 | object:nil]; | 266 | object:nil]; |
232 | [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil]; | 267 | [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil]; |
268 | /* Media player remote controls. */ | ||
269 | MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter]; | ||
270 | [[commandCenter pauseCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { | ||
271 | iPlayer *player = active_Player(); | ||
272 | if (player) { | ||
273 | setPaused_Player(player, iTrue); | ||
274 | return MPRemoteCommandHandlerStatusSuccess; | ||
275 | } | ||
276 | return MPRemoteCommandHandlerStatusCommandFailed; | ||
277 | }]; | ||
278 | [[commandCenter playCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { | ||
279 | iPlayer *player = active_Player(); | ||
280 | if (player) { | ||
281 | if (isPaused_Player(player)) { | ||
282 | setPaused_Player(player, iFalse); | ||
283 | } | ||
284 | else { | ||
285 | start_Player(player); | ||
286 | } | ||
287 | return MPRemoteCommandHandlerStatusSuccess; | ||
288 | } | ||
289 | return MPRemoteCommandHandlerStatusCommandFailed; | ||
290 | }]; | ||
291 | [[commandCenter stopCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { | ||
292 | iPlayer *player = active_Player(); | ||
293 | if (player) { | ||
294 | stop_Player(player); | ||
295 | return MPRemoteCommandHandlerStatusSuccess; | ||
296 | } | ||
297 | return MPRemoteCommandHandlerStatusCommandFailed; | ||
298 | }]; | ||
299 | [[commandCenter togglePlayPauseCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { | ||
300 | iPlayer *player = active_Player(); | ||
301 | if (player) { | ||
302 | setPaused_Player(player, !isPaused_Player(player)); | ||
303 | return MPRemoteCommandHandlerStatusSuccess; | ||
304 | } | ||
305 | return MPRemoteCommandHandlerStatusCommandFailed; | ||
306 | }]; | ||
307 | [[commandCenter nextTrackCommand] setEnabled:NO]; | ||
308 | [[commandCenter previousTrackCommand] setEnabled:NO]; | ||
309 | [[commandCenter changeRepeatModeCommand] setEnabled:NO]; | ||
310 | [[commandCenter changeShuffleModeCommand] setEnabled:NO]; | ||
311 | [[commandCenter changePlaybackRateCommand] setEnabled:NO]; | ||
312 | [[commandCenter seekForwardCommand] setEnabled:NO]; | ||
313 | [[commandCenter seekBackwardCommand] setEnabled:NO]; | ||
314 | [[commandCenter skipForwardCommand] setEnabled:NO]; | ||
315 | [[commandCenter skipBackwardCommand] setEnabled:NO]; | ||
316 | [[commandCenter changePlaybackPositionCommand] setEnabled:NO]; | ||
233 | } | 317 | } |
234 | 318 | ||
235 | static iBool isDarkMode_(iWindow *window) { | 319 | static iBool isDarkMode_(iWindow *window) { |
@@ -265,7 +349,7 @@ iBool isPhone_iOS(void) { | |||
265 | } | 349 | } |
266 | 350 | ||
267 | int displayRefreshRate_iOS(void) { | 351 | int displayRefreshRate_iOS(void) { |
268 | return uiWindow_(get_Window()).screen.maximumFramesPerSecond; | 352 | return (int) uiWindow_(get_Window()).screen.maximumFramesPerSecond; |
269 | } | 353 | } |
270 | 354 | ||
271 | void setupWindow_iOS(iWindow *window) { | 355 | void setupWindow_iOS(iWindow *window) { |
@@ -279,7 +363,10 @@ void playHapticEffect_iOS(enum iHapticEffect effect) { | |||
279 | HapticState *hs = (HapticState *) appState_.haptic; | 363 | HapticState *hs = (HapticState *) appState_.haptic; |
280 | switch(effect) { | 364 | switch(effect) { |
281 | case tap_HapticEffect: | 365 | case tap_HapticEffect: |
282 | [hs playTapEffect]; | 366 | [hs playHapticEffect:hs.tapDef]; |
367 | break; | ||
368 | case gentleTap_HapticEffect: | ||
369 | [hs playHapticEffect:hs.gentleTapDef]; | ||
283 | break; | 370 | break; |
284 | } | 371 | } |
285 | } | 372 | } |
@@ -324,6 +411,38 @@ iBool processEvent_iOS(const SDL_Event *ev) { | |||
324 | return iFalse; /* allow normal processing */ | 411 | return iFalse; /* allow normal processing */ |
325 | } | 412 | } |
326 | 413 | ||
414 | void updateNowPlayingInfo_iOS(void) { | ||
415 | const iPlayer *player = active_Player(); | ||
416 | if (!player) { | ||
417 | clearNowPlayingInfo_iOS(); | ||
418 | return; | ||
419 | } | ||
420 | NSMutableDictionary<NSString *, id> *info = [[NSMutableDictionary<NSString *, id> alloc] init]; | ||
421 | [info setObject:[NSNumber numberWithDouble:time_Player(player)] | ||
422 | forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime]; | ||
423 | [info setObject:[NSNumber numberWithInt:MPNowPlayingInfoMediaTypeAudio] | ||
424 | forKey:MPNowPlayingInfoPropertyMediaType]; | ||
425 | [info setObject:[NSNumber numberWithDouble:duration_Player(player)] | ||
426 | forKey:MPMediaItemPropertyPlaybackDuration]; | ||
427 | const iString *title = tag_Player(player, title_PlayerTag); | ||
428 | const iString *artist = tag_Player(player, artist_PlayerTag); | ||
429 | if (isEmpty_String(title)) { | ||
430 | title = collectNewCStr_String("Audio"); /* TODO: Use link label or URL file name */ | ||
431 | } | ||
432 | if (isEmpty_String(artist)) { | ||
433 | artist = collectNewCStr_String("Lagrange"); /* TODO: Use domain or base URL */ | ||
434 | } | ||
435 | [info setObject:[NSString stringWithUTF8String:cstr_String(title)] | ||
436 | forKey:MPMediaItemPropertyTitle]; | ||
437 | [info setObject:[NSString stringWithUTF8String:cstr_String(artist)] | ||
438 | forKey:MPMediaItemPropertyArtist]; | ||
439 | [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:info]; | ||
440 | } | ||
441 | |||
442 | void clearNowPlayingInfo_iOS(void) { | ||
443 | [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:nil]; | ||
444 | } | ||
445 | |||
327 | void exportDownloadedFile_iOS(const iString *path) { | 446 | void exportDownloadedFile_iOS(const iString *path) { |
328 | NSURL *url = [NSURL fileURLWithPath:[[NSString alloc] initWithCString:cstr_String(path) | 447 | NSURL *url = [NSURL fileURLWithPath:[[NSString alloc] initWithCString:cstr_String(path) |
329 | encoding:NSUTF8StringEncoding]]; | 448 | encoding:NSUTF8StringEncoding]]; |
@@ -335,6 +454,17 @@ void exportDownloadedFile_iOS(const iString *path) { | |||
335 | [viewController_(get_Window()) presentViewController:picker animated:YES completion:nil]; | 454 | [viewController_(get_Window()) presentViewController:picker animated:YES completion:nil]; |
336 | } | 455 | } |
337 | 456 | ||
457 | void pickFileForOpening_iOS(void) { | ||
458 | UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] | ||
459 | initWithDocumentTypes:@[@"fi.skyjake.lagrange.gemini", | ||
460 | @"public.text", | ||
461 | @"public.image", | ||
462 | @"public.audio"] | ||
463 | inMode:UIDocumentPickerModeImport]; | ||
464 | picker.delegate = appState_; | ||
465 | [viewController_(get_Window()) presentViewController:picker animated:YES completion:nil]; | ||
466 | } | ||
467 | |||
338 | /*----------------------------------------------------------------------------------------------*/ | 468 | /*----------------------------------------------------------------------------------------------*/ |
339 | 469 | ||
340 | enum iAVFAudioPlayerState { | 470 | enum iAVFAudioPlayerState { |