-
Notifications
You must be signed in to change notification settings - Fork 0
/
AppController.m
388 lines (336 loc) · 12 KB
/
AppController.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
//
// AppController.m
// FanRadio
//
// Created by Du Song on 10-6-16.
// Copyright 2010 rollingcode.org. All rights reserved.
//
#import "AppController.h"
#import "DataLoader.h"
#import "Speaker.h"
#import "PerProcessHTTPCookieStore.h"
#import "FRPrefController.h"
@implementation FanRadioApplication
- (void)sendEvent:(NSEvent *)theEvent {
// If event tap is not installed, handle events that reach the app instead
BOOL shouldHandleMediaKeyEventLocally = ![SPMediaKeyTap usesGlobalMediaKeyTap];
if(shouldHandleMediaKeyEventLocally && [theEvent type] == NSSystemDefined && [theEvent subtype] == SPSystemDefinedEventMediaKeys) {
[(id)[self delegate] mediaKeyTap:nil receivedMediaKeyEvent:theEvent];
}
[super sendEvent:theEvent];
}
@end
@implementation AppController
@synthesize useMediaKeys = useMediaKeys;
static AppController * _instance = nil;
static NSMutableAttributedString * _heart;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
keyTap = [[SPMediaKeyTap alloc] initWithDelegate:self];
if([SPMediaKeyTap usesGlobalMediaKeyTap])
[keyTap startWatchingMediaKeys];
else
FWLog(@"Media key monitoring disabled");
}
-(void)mediaKeyTap:(SPMediaKeyTap*)keyTap receivedMediaKeyEvent:(NSEvent*)event {
if (!useMediaKeys) return;
NSAssert([event type] == NSSystemDefined && [event subtype] == SPSystemDefinedEventMediaKeys, @"Unexpected NSEvent in mediaKeyTap:receivedMediaKeyEvent:");
// here be dragons...
int keyCode = (([event data1] & 0xFFFF0000) >> 16);
int keyFlags = ([event data1] & 0x0000FFFF);
BOOL keyIsPressed = (((keyFlags & 0xFF00) >> 8)) == 0xA;
int keyRepeat = (keyFlags & 0x1);
if (keyIsPressed) {
//NSString *debugString = [NSString stringWithFormat:@"%@", keyRepeat?@", repeated.":@"."];
switch (keyCode) {
case NX_KEYTYPE_PLAY:
if ([pauseItem isHidden]) {
[self resume:nil];
} else {
[self pause:nil];
}
break;
case NX_KEYTYPE_FAST:
[self doShuffle:nil];
break;
case NX_KEYTYPE_REWIND:
[self like:nil];
break;
default:
//debugString = [NSString stringWithFormat:@"Key %d pressed%@", keyCode, debugString];
break;
// More cases defined in hidsystem/ev_keymap.h
}
}
}
+ (AppController *) instance {
return _instance;
}
#pragma mark menu actions
- (void)markNormal {
[self syncPlayState:[resumeItem isHidden]];
}
- (void)markHappy {
[statusItem setAttributedTitle:_heart];
}
- (void)markBuffer {
[statusItem setTitle:@"턢"];
}
- (IBAction)like:(id)sender {
if ([likeItem state]) {
[radio unlikeCurrent];
[likeItem setState:0];
[self markNormal];
} else {
[radio likeCurrent];
[likeItem setState:1];
[self markHappy];
}
}
- (IBAction)dislike:(id)sender {
[radio banCurrent];
}
- (IBAction)tuneChannel:(id)sender {
if (lastChannelItem) [lastChannelItem setState:0];
lastChannelItem = sender;
[sender setState:1];
currentChannel = [[FRChannelList instance] channelByTag:[sender tag]];
[currentChannel tune];
[[NSUserDefaults standardUserDefaults] setObject:[currentChannel uuid] forKey:@"LastChannel"];
}
- (IBAction)doShuffle:(id)sender {
[radio endAndPlayNext];
}
- (IBAction)endAndPlayNext:(id)sender {
[radio endAndPlayNext];
}
- (IBAction)openPage:(id)sender {
if (radio.pageURL) {
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:radio.pageURL]];
}
}
- (IBAction)openUserPage:(id)sender {
if (radio.loginSuccess) {
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:radio.profilePage]];
} else {
//FIXME [settingsPane orderFront:nil];
}
}
- (IBAction)openPreferences:(id)sender{
//[FRPrefController showWindow];
//[NSBundle loadNibNamed:@"Preferences" owner:@"foo"];
[FRPrefController showWindow:self];
/*static NSWindowController *wc = nil;
if (!wc) wc = [[NSWindowController alloc] initWithWindowNibName:@"Preferences"];
[wc showWindow:self];
[[wc window] makeKeyAndOrderFront:self];*/
}
- (IBAction)pause:(id)sender {
[Speaker pause];
[self syncPlayState:NO];
}
- (IBAction)resume:(id)sender{
[Speaker resume];
[self syncPlayState:YES];
}
#pragma mark app events
- (void)awakeFromNib {
FWLog(@"awakeFromNib");
_instance = self;
pendingPlay = [[NSUserDefaults standardUserDefaults] boolForKey:@"PlayOnLaunch"];
lastPlayStarted = 0;
currentChannel = nil;
//check login info
radio = [DoubanRadio instance];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(songEnded:) name:SongEndedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(songReady:) name:SongReadyNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateUser:) name:LoginCheckedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateChannels:) name:ChannelListLoadedNotification object:nil];
[radio checkLogin];
[GrowlApplicationBridge setGrowlDelegate:self];
//Keyboard Shortcut
useMediaKeys = [[NSUserDefaults standardUserDefaults] boolForKey:@"UseMediaKeys"];
//show icon
//float width = 24.0;
//float height = [[NSStatusBar systemStatusBar] thickness];
//NSRect viewFrame = NSMakeRect(0, 0, width, height);
statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength] retain];
//[statusItem setView:[[[StatusItemView alloc] initWithFrame:viewFrame controller:self] autorelease]];
[statusItem setToolTip:@"Fan Radio"];
[statusItem setHighlightMode:YES];
[statusItem setMenu:statusMenu];
//load channel
[self syncPlayState:FALSE];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(songBuffering:) name:SongBufferingNotification object:nil];
FWLog(@"awaken");
}
- (void)loginInProgress:(NSNotification *)notification {
[usernameItem setTitle:@"Logging in..."];
[usernameItem setEnabled:NO];
}
- (void)updateUser:(NSNotification *)notification {
[usernameItem setTitle:(radio.loginSuccess ? radio.nickname : @"Not Logged In")];
[usernameItem setEnabled:YES];
if (![[FRChannelList instance].channels count]) {
[[DoubanRadio instance] loadChannelList];
}
}
- (void)updateChannels:(NSNotification *)notification {
int i = [statusMenu indexOfItem:channelsItem] + 1;
NSMenuItem *m;
while ((m = [statusMenu itemAtIndex:i]) != nil) {
if (!m.tag) break;
[statusMenu removeItem:m];
}
for (FRChannel *c in [FRChannelList instance].channels){
m = [[NSMenuItem alloc] initWithTitle:(_inChinese ? c.name : c.nameEng) action:@selector(tuneChannel:) keyEquivalent:@""];
m.tag = c.tag;
[m setIndentationLevel:1];
[statusMenu insertItem:m atIndex:i++];
[m release];
}
currentChannel = [[FRChannelList instance] channelByUUID:[[NSUserDefaults standardUserDefaults] objectForKey:@"LastChannel"]];
if (pendingPlay) {
pendingPlay = NO;
[radio tuneToChannel:currentChannel];
}
if (!currentChannel) {
currentChannel = [[FRChannelList instance].channels objectAtIndex:0];
}
lastChannelItem = [statusMenu itemWithTag:currentChannel.tag];
[lastChannelItem setState:1];
FWLog(@"Last Channel %@ %@", currentChannel.name, lastChannelItem.title);
}
- (void)playNext {
[self performSelector:@selector(doShuffle:) withObject:nil afterDelay:1];
}
#define MAX_PLAY_TIME 30 * 60
#define MIN_PLAY_TIME 30 // ignore short clips, usually ads or unliked songs
- (void)songEnded:(NSNotification *)notification {
FWLog(@"song ended");
if (lastPlayStarted > 0) {
NSTimeInterval t = time(nil) - lastPlayStarted;
if (t > MIN_PLAY_TIME) {
if (t > MAX_PLAY_TIME) t = MAX_PLAY_TIME;
radio.totalListenedTime = radio.totalListenedTime + t;
radio.totalListenedTracks ++;
}
}
lastPlayStarted = 0;
[self performSelector:@selector(endAndPlayNext:) withObject:nil afterDelay:0.1];
}
- (void)songReady:(NSNotification *)notification {
FWLog(@"song ready");
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(coverLoaded:) name:DataLoadedNotification object:[DataLoader load:radio.cover]];
}
- (void)songBuffering:(NSNotification *)notification {
[self markBuffer];
}
- (void)coverLoaded:(NSNotification *)notification {
FWLog(@"cover loaded");
[[NSNotificationCenter defaultCenter] removeObserver:self name:DataLoadedNotification object:[notification object]];
NSData *data = [[notification userInfo] objectForKey:@"data"];
[Speaker play:radio.url];
lastPlayStarted = time(nil);
[songTitleItem setTitle:[NSString stringWithFormat:@"%@ - %@", radio.title, radio.artist]];
[likeItem setState:radio.liked];
NSImage *img = [[NSImage alloc] initWithData:data];
NSSize sz = img.size;
if (sz.height > 128) {
sz.width = sz.width * 128 / sz.height;
sz.height = 128;
[img setSize:sz];
}
[coverItem setImage:img];
[img release];
[coverItem setHidden:NO];
[GrowlApplicationBridge notifyWithTitle:radio.title
description:[NSString stringWithFormat:@"%@\n%@", radio.artist, radio.album]
notificationName:@"Song Start"
iconData:data
priority:0
isSticky:NO
clickContext:nil];
if (radio.liked) [self markHappy]; else [self markNormal];
[self syncPlayState:TRUE];
}
- (void)dealloc {
[[NSStatusBar systemStatusBar] removeStatusItem:statusItem];
[statusMenu release];
[coverItem release];
[songTitleItem release];
[likeItem release];
[usernameItem release];
[pauseItem release];
[resumeItem release];
[super dealloc];
}
#if USE_SHORTCUT
- (void)hitHotKey:(PTHotKey *)hotKey {
@"hitHotKey %zu", [[hotKey identifier] tag]);
switch ([[hotKey identifier] tag]) {
case 0: //shuffle
[self doShuffle:nil];
break;
case 1: //like
[self like:nil];
break;
case 2: //ban
[self dislike:nil];
break;
}
}
#endif
- (NSArray *)feedParametersForUpdater:(id)updater sendingSystemProfile:(BOOL)sendingProfile {
FWLog(@"feedParametersForUpdater");
NSMutableArray *ret = [NSMutableArray array];
NSString *uiid_ = [self uiid];
NSString *time_ = [NSString stringWithFormat:@"%d", [radio totalListenedTime]];
NSString *tracks_ = [NSString stringWithFormat:@"%d", [radio totalListenedTracks]];
[ret addObject:[NSDictionary dictionaryWithObjectsAndKeys:uiid_, @"value", @"uiid", @"key", nil]];
[ret addObject:[NSDictionary dictionaryWithObjectsAndKeys:time_, @"value", @"time", @"key", nil]];
[ret addObject:[NSDictionary dictionaryWithObjectsAndKeys:tracks_, @"value", @"trck", @"key", nil]];
return ret;
}
- (NSString *) uiid {
NSString *uiid_ = [[NSUserDefaults standardUserDefaults] stringForKey:@"UniqueInstallationId"];
if (!uiid_) {
srandom(time(NULL));
uiid_ = [NSString stringWithFormat:@"%8x%8x", random(), random()];
[[NSUserDefaults standardUserDefaults] setValue:uiid_ forKey:@"UniqueInstallationId"];
}
return uiid_;
}
- (void) syncPlayState:(BOOL)isPlaying {
if (isPlaying) {
FWLog(@"pause enabled");
[resumeItem setHidden:YES];
[pauseItem setHidden:NO];
[statusItem setTitle:@"♫"];
} else {
FWLog(@"resume enabled");
[resumeItem setHidden:NO];
[pauseItem setHidden:YES];
[statusItem setTitle:@"♪"];
}
}
+ (void)initialize {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSDictionary *appDefaults = [NSDictionary
dictionaryWithObject:[NSNumber numberWithInteger:0] forKey:@"DoubanChannel"];
[defaults registerDefaults:appDefaults];
#if COOKIE_MAGIC
[PerProcessHTTPCookieStore makeSurePerProcessHTTPCookieStoreLinkedIn];
#endif
_heart = [[NSMutableAttributedString alloc] initWithString:@"❤"];
[_heart addAttribute:NSFontAttributeName value:[NSFont fontWithName:@"Helvetica" size:14] range:NSMakeRange(0,1)];
[_heart addAttribute:NSForegroundColorAttributeName value:[NSColor colorWithDeviceRed:1 green:0.4 blue:0.4 alpha:1] range:NSMakeRange(0,1)];
NSString *lang = [[NSLocale preferredLanguages] objectAtIndex:0];
FWLog(@"preferredLanguage: %@", lang);
_inChinese = [lang hasPrefix:@"zh"];
// Register defaults for the whitelist of apps that want to use media keys
[[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
[SPMediaKeyTap defaultMediaKeyUserBundleIdentifiers], kMediaKeyUsingBundleIdentifiersDefaultsKey,
[NSNumber numberWithBool:YES], @"PlayOnLaunch",
nil]];
}
@end