Hatena::ブログ(Diary)

さよならストレス

2016-01-30

Firefox 44 で Hatena Bookmark 拡張が動かなくなったので、Google Chrome 用の拡張を流用してみた話

Firefox 44 をインストールして Hatena Bookmark 拡張が動かなくなってしまいました。

個人的にはブコメが見られれば良いだけなので自作してみようかとも考えましたが、

ふと、「新しいfirefoxのweb extensionsがChromeExtentionとの互換性そのままだった」 を思い出し、

いけるのでは?と思い立ってやってみました。

Google Chrome 用の拡張をコピー

Windows な方なら ChromeHantea Bookmark 拡張 をインストールすると以下に展開されているかと思います。

C:\Users\{自分のユーザー名}\AppData\Local\Google\Chrome\User Data\Default\Extensions\dnlfpnhinnjdgmjfpccajboogcjocdla

f:id:wwwcfe:20160130121626p:image

記事執筆時点で1.4.4_0 というバージョンが入っていました。

「新しいfirefoxのweb extensionsがChromeExtentionとの互換性そのままだった」 の記事を参考に、

manifest.json に JSON として有効になるようカンマなど漏れがないか注意して以下を記入します。

今回は先頭の方に追加したので、お尻にカンマを付けています。

   "applications": {
      "gecko": {
         "id": "borderify@mozilla.org"
      }
   },

あとはディレクトリ内のファイル一式を zip で圧縮し、拡張子を xpi にしておきます。

これでとりあえずの準備は完了しました。


署名に関する警告を無効化

そのままでは署名に関する警告が出てしまい、インストール出来ません。

安全ではないですが、一時的に設定を変更することでインストールを許可しておきます。

http://blog.halpas.com/archives/8371 を参考に about:config で xpinstall.signatures.required を false にします。

実験が終わったら xpinstall.signatures.required を true に戻すのを忘れないようにしておきます。


で、インストールしてみると・・・

Firefox のアドオンのページに固めた xpi をドラッグドロップしてインストールします。

するとなんだか動きそうな感じが!??

f:id:wwwcfe:20160130121830p:image

f:id:wwwcfe:20160130121829p:image

f:id:wwwcfe:20160130121827p:image

ためしに Google のブコメを開いてみようとすると白紙に・・・。

f:id:wwwcfe:20160130121825p:image

ブラウザコンソールを開いてみると以下のようなエラーが出ていました。

TypeError: BG.chrome.tabs.getSelected is not a function
 popup.js:95:9

f:id:wwwcfe:20160130121823p:image

調べてみると、chrome.tabs.getSelected は廃止予定らしく、Firefox には実装されていないのかもしれません。

https://developer.chrome.com/extensions/tabs#method-getSelected によれば、

Deprecated since Chrome 33. Please use tabs.query {active: true}.

となっているので、そのように書き換えてみます。

先ほど修正した manifest.json のディレクトリを辿っていくと、background/popup.js があるので95行目当たりを以下のように修正してみます。

修正前

    if (popupMode) {
        BG.chrome.tabs.getSelected(null, function(tab) {
            d.call({
                url: tab.url,
                faviconUrl: tab.faviconUrl,
                winId: tab.windowId,
                tabId: tab.id,
                title: tab.title,
            });
        });
    } else {

修正後

    if (popupMode) {
        BG.chrome.tabs.query({active: true, lastFocusedWindow: true}, function(tabs) { // ここと
            var tab = tabs[0]; // ここ
            d.call({
                url: tab.url,
                faviconUrl: tab.faviconUrl,
                winId: tab.windowId,
                tabId: tab.id,
                title: tab.title,
            });
        });
    } else {

再インストール

で、再度 zip に圧縮して xpi として Firefox にインストールしてみます。

すると・・・

f:id:wwwcfe:20160130121821p:image

動きました!

ただ、他にも修正しないといけない箇所があるようで、完璧ではありません。

とりあえず「ブコメを見る」という目的は達成できたのでよしとします。

もともと はてなブックマーク Google Chrome 拡張ソースコードは公開されているようなので、Pull Request などを送ると対応してくれるかもしれません。

が、いろいろ検証するのが面倒なのでどなたかお願いします

m(_ _)m

そもそもで言えば Firefoxの拡張(ソースコード)が直ると良いんですが・・・


まとめ

Hatena Bookmark Chrome 用の拡張が (ほとんどそのままで) Firefox でも動く事が分かりました。

意外とあっさり動いてしまったので自作しなくて良かったです。


追記 (2016-02-02)

はてなブックマーク Firefox 拡張 v2.3.9 で修正されたもよう。

良かった。

2014-09-20

そうだ、iOS8 の App Extension ではてなブックマークコメントビューアを作ろう!

iOS8ついにでましたね。App Extension が使えるようになって LastPass や 1Password などの連携がとても便利です。

はてなさんも ブックマークに追加する App Extension を実装されたようですが、

それよりも私としてはブコメをさくっと見たいのです。というわけで実装してみました。

AppStore に出そうかとも思いましたが、App Extension 意外にメインの機能が無いので審査通らなそうなのと気力が無いのでやめておきます。

はてなさん公式で追加してもらえませんかね。チラリ。id:hatenatech さんよろしくお願いします。

プロジェクト一式は Github で。

下準備

  • Xcode6 をインストール (更新とか面倒くさいので Mac App Store から入れました)
  • Xcode6 を起動し、[File] -> [New] -> [Project] を開く
  • [iOS] -> [Application] から適当に選んでプロジェクトを作る (一切触らないので何でも良い)
  • とりあえず [Master-Detail Application] とか [Single View Application] とかで。

入力内容は以下のような感じで

f:id:wwwcfe:20140920180433p:image

ここまではいつもの手順ですね。


Action Extension を実装

[File] -> [New] -> [Target] からターゲットを追加します。このときに [Action Extension] を選びます。

f:id:wwwcfe:20140920180434p:image

f:id:wwwcfe:20140920180435p:image

上記のように入力し作成が完了すると、

  • Viewer/ActionViewController.m
  • Viewer/MainInterface.storyboard

などが追加されています。今回は上記二つのファイルしか編集しません。

f:id:wwwcfe:20140920180700p:image


あとは UI とコードを編集していきます。UI は UITableView をおいて Dynamic Prototype Cell を作り、制約を良い感じに設定します。

f:id:wwwcfe:20140920180701p:image


コードはざっくりと以下のようなことを行います。

  • Safari などから URL を受け取り
  • ブックマーク情報を取得
  • テーブルの更新
  • ユーザープロフィール画像を取得

あまり行儀がよろしくないと思いますが Cell のサブクラス化が面倒なので、Storyboard上で Tag を付けて取得しています。

ポイントは以下の2行で、セルの高さを自動調整しています。iOS8、すごいらくちんですね。

self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 80.f;

コード全体は以下の通りです

#import "ActionViewController.h"
#import <MobileCoreServices/MobileCoreServices.h>

@interface ActionViewController () <UITableViewDataSource, UITableViewDelegate>

@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (weak, nonatomic) IBOutlet UIBarButtonItem *countButton;

@property (strong, nonatomic) NSArray *bookmarks;

@end

@implementation ActionViewController

- (void)viewDidLoad {
	[super viewDidLoad];

	// 追加。セルの高さを自動で良い感じにする。
	self.tableView.rowHeight = UITableViewAutomaticDimension;
	self.tableView.estimatedRowHeight = 80.f;

	BOOL found = NO;
	for (NSExtensionItem *item in self.extensionContext.inputItems) {
		for (NSItemProvider *itemProvider in item.attachments) {

			// URL だけ取り出す
			if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeURL]) {
				__weak typeof(self) wself = self;
				[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeURL options:nil completionHandler:^(NSURL *item, NSError *error) {
					[wself loadURL:item];
				}];

				found = YES;
				break;
			}
		}

		if (found) {
			// 最初の一個しかみないので break する
			break;
		}
	}
}

- (void)loadURL:(NSURL *)url {
	NSString *escaped = [url.absoluteString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
	NSString *endpoint = [NSString stringWithFormat:@"http://b.hatena.ne.jp/entry/jsonlite/?url=%@", escaped];
	__weak typeof(self) wself = self;
	NSLog(@"%@", endpoint);
	NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:endpoint]];
	[NSURLConnection sendAsynchronousRequest:req queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
		NSDictionary *d = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
		NSLog(@"%@", d);
		[wself updateViewWithDictionary:d];
	}];
}

- (void)updateViewWithDictionary:(NSDictionary *)d {
	NSNumber *count = d[@"count"];
	NSArray *bookmarks = d[@"bookmarks"];
	self.countButton.title = [NSString stringWithFormat:@"%@", count];
	self.bookmarks = bookmarks;
	[self.tableView reloadData];
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
	return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
	return self.bookmarks.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	static NSString *CellIdentifier = @"Cell";
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

	NSDictionary *bookmark = self.bookmarks[indexPath.row];
	NSString *comment = bookmark[@"comment"];
	NSString *user = bookmark[@"user"];
	NSString *timestamp = bookmark[@"timestamp"];
	//NSArray *tags = bookmark[@"tags"];

	__weak UIImageView *imageView = (UIImageView *)[cell viewWithTag:100];
	imageView.image = nil;
	[self loadImageWithUserID:user completionHandler:^(UIImage *image) {
		imageView.image = image;
	}];

	UILabel *userLabel = (UILabel *)[cell viewWithTag:200];
	userLabel.text = user;

	UILabel *timestampLabel = (UILabel *)[cell viewWithTag:300];
	timestampLabel.text = timestamp;

	UILabel *commentLabel = (UILabel *)[cell viewWithTag:400];
	commentLabel.text = comment;
	return cell;
}

- (void)loadImageWithUserID:(NSString *)userID completionHandler:(void(^)(UIImage *image))handler {
	static NSCache *cache = nil;
	if (!cache) {
		cache = [[NSCache alloc] init];
		cache.countLimit = 1000;
	}

	NSString *s = @"http://n.hatena.com/%@/profile/image.gif?type=face&size=64";
	NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:s, userID]];

	// キャッシュから取り出す
	UIImage *cachedImage = [cache objectForKey:url.absoluteString];
	if (cachedImage) {
		if (handler) handler(cachedImage);
		return;
	}

	// なれけば通信して取得
	NSURLRequest *req = [NSURLRequest requestWithURL:url];
	[NSURLConnection sendAsynchronousRequest:req queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
		UIImage *image = [UIImage imageWithData:data];
		if (!image) {
			if (handler) {
				handler(nil);
			}
			return;
		}

		[cache setObject:image forKey:url.absoluteString];
		if (handler) {
			handler(image);
		}
	}];
}

- (IBAction)done {
	// Return any edited content to the host app.
	// This template doesn't do anything, so we just echo the passed in items.
	[self.extensionContext completeRequestReturningItems:self.extensionContext.inputItems completionHandler:nil];
}

@end

実行してみる

実行するターゲットが App Extension になっていることを確認し、[Product] -> [Run] します。

f:id:wwwcfe:20140920180702p:image

[Choose an app to run:] というダイアログが出てくるので、[Safari] を選択し、[Run] します。

f:id:wwwcfe:20140920180703p:image

すると画像のように Action Extension が表示されます。(Comments というやつ)

f:id:wwwcfe:20140920180704p:image

あとはこれをタップすると・・・

f:id:wwwcfe:20140920180705p:image

表示されました!


おしまい

結構簡単にできてしまいました。はてなさん、ぜひ公式でビューアの方もお願いします m(_ _)m