Firefox 44 で Hatena Bookmark 拡張が動かなくなったので、Google Chrome 用の拡張を流用してみた話
Firefox 44 をインストールして Hatena Bookmark 拡張が動かなくなってしまいました。
個人的にはブコメが見られれば良いだけなので自作してみようかとも考えましたが、
ふと、「新しいfirefoxのweb extensionsがChromeExtentionとの互換性そのままだった」 を思い出し、
いけるのでは?と思い立ってやってみました。
Google Chrome 用の拡張をコピー
Windows な方なら Chrome で Hantea Bookmark 拡張 をインストールすると以下に展開されているかと思います。
C:\Users\{自分のユーザー名}\AppData\Local\Google\Chrome\User Data\Default\Extensions\dnlfpnhinnjdgmjfpccajboogcjocdla
記事執筆時点で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 をドラッグドロップしてインストールします。
するとなんだか動きそうな感じが!??
ためしに Google のブコメを開いてみようとすると白紙に・・・。
ブラウザコンソールを開いてみると以下のようなエラーが出ていました。
TypeError: BG.chrome.tabs.getSelected is not a function popup.js:95:9
調べてみると、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 にインストールしてみます。
すると・・・
動きました!
ただ、他にも修正しないといけない箇所があるようで、完璧ではありません。
とりあえず「ブコメを見る」という目的は達成できたのでよしとします。
もともと はてなブックマーク Google Chrome 拡張 でソースコードは公開されているようなので、Pull Request などを送ると対応してくれるかもしれません。
が、いろいろ検証するのが面倒なのでどなたかお願いします
m(_ _)m
そうだ、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] とかで。
入力内容は以下のような感じで
ここまではいつもの手順ですね。
Action Extension を実装
[File] -> [New] -> [Target] からターゲットを追加します。このときに [Action Extension] を選びます。
上記のように入力し作成が完了すると、
- Viewer/ActionViewController.m
- Viewer/MainInterface.storyboard
などが追加されています。今回は上記二つのファイルしか編集しません。
あとは UI とコードを編集していきます。UI は UITableView をおいて Dynamic Prototype Cell を作り、制約を良い感じに設定します。
コードはざっくりと以下のようなことを行います。
あまり行儀がよろしくないと思いますが 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] します。
[Choose an app to run:] というダイアログが出てくるので、[Safari] を選択し、[Run] します。
すると画像のように Action Extension が表示されます。(Comments というやつ)
あとはこれをタップすると・・・
表示されました!
おしまい
結構簡単にできてしまいました。はてなさん、ぜひ公式でビューアの方もお願いします m(_ _)m
弱小チームでも大丈夫!? 社内で GitHub Enterprise モドキを運用するなら gogs が超簡単!!
社内で GitHub Enterprise を利用したいのですが、そんな予算もないという小さなチームに所属している方に朗報です。
Go 言語で書かれた gogs (Go Git Service) という OSS が本日公開されました。
(開発自体は以前から行われていましたが、v0.2.0 が最初のリリースのようです)
gogits/gogs - Github
Go 言語で書かれているため、Windows でも、Linux でも、Mac でも動かせます。データベースは SQLite3 を選択できるので、どの環境でも手軽に環境を構築して、いらなくなったらポイするのも簡単です。Gitlab も良いのですが、構築がつらいので・・・。
いやぁ、Go 言語、きてますね。熱すぎる。
画像はリンク先の「オリジナルサイズを表示」で詳しく表示されます。
インストール
インストールは簡単なのでさくっとやってみます。今回は MacOS X 10.9 に gogs を構築します。
といってもやることは下記からバイナリをダウンロードして展開するだけです。
SQLite3 を利用したいので「SQLite3 built-in(64-bit machine only): Windows - Linux - Mac OS X 」のからダウンロードします。
https://github.com/gogits/gogs/wiki/Install-from-binary
ダウンロードが終わったらファイルを展開します。
展開が終わったら Terminal.app で gogs を展開したディレクトリに移動し、以下のコマンドを実行します。
$ cd path/to/gogs
$ ./gogs web
問題なく起動できれば http://localhost:3000/ にアクセスします。
初回はインストール画面にリダイレクトします。http://localhost:3000/install
DBを「SQLite3」に変更して、管理者ユーザーを 「adminuser」 として作成します。
Run user は今回はとりあえずさくっと構築するだけなので、ログインユーザーと同じにしました。実際に利用するときは 「git」 のままのほうが良さげな気はします。
さくっと環境構築が完了したので、ログインします。
以下、スクリーンショットをご覧ください。
clone/commit/push してみる
SSHキーを登録しておけば以下のように clone してきて編集してコミット、プッシュも可能です。
レポジトリを作成してみます
SSH キーを登録して
Mac の環境設定でリモートログインを有効にしておきます。
clone/commit/push してみます
$ git clone admin@localhost:adminuser/hello.git $ cd hello $ vim README.md $ git add . $ git commit -m "Hoge" $ git push origin master
おお、できますた!
今後に期待十分!
いかがでしょうか。最初のリリースにしてはかなりできが良いように感じました。
Github Flow 的な使い方や CI連携などをする場合は考える必要がありますが、今後に十分期待できるのではないでしょうか。
配送状況をさくっと確認できる Alfred Workflow を作りました (ヤマト運輸・佐川急便・日本郵便・西濃運輸に対応)
Alfred Workflow が便利なので何か作ってみようと思い、各種運輸業者の配送状況をさくっと確認できる Workflow を作ってみました。
作ってる途中でこれカスタムサーチでよくね?と思ったので、両方つくりました。
非 PowerPack ユーザーはカスタムサーチをご利用ください。
以下のように入力するとで利用できます。track まで入力すると、以下の4つの候補がでてくるので、track 以降は入力せず候補から選択する感じです。
track yamato <送り状番号> track sagawa <送り状番号> track japanpost <送り状番号> track seino <送り状番号>
ダウンロードは以下から
カスタムサーチで登録したい方
README.md のカスタムサーチセクションを参照してください
https://github.com/wwwcfe/alfred-jp-track/blob/master/README.md
これは便利すぎる! 標準 iOS7 SDK のみで QR コードが読み取れるようになった!!
iOS 開発で QR コードや JAN バーコードを読み取ろうかと思ったら、ZXing, ZXingObjC, ZBar などの利用が考えられますが、ZXing は組み込みが少し面倒で、ZingObjC は導入が簡単でも動作が(ZXing, ZBar に比べて)少し遅く、ZBar は認識が早いがライセンスが LGPL でクライアントへの提案が難しかったりとしっくりくる物がなかったのですが、iOS7 アップデートで標準 SDK に QR の読み取り機能が追加されたおかげで一気に問題が解決しそうです。iOS7 SDK 以降というのが少し痛いですが標準 SDK で利用できるというのは大きな利点です。
早速実装コード
以下、実装コードです。iOS7 SDK, Xcode 5 で、 Single View Application を選択して、ViewController を実装します。
ビルド設定でAVFoundation のリンクを忘れずに!
#import <AVFoundation/AVFoundation.h> #import "QRViewController.h" @interface QRViewController () <AVCaptureMetadataOutputObjectsDelegate> @property (strong, nonatomic) AVCaptureSession *session; @end @implementation QRViewController - (void)viewDidLoad { [super viewDidLoad]; self.session = [[AVCaptureSession alloc] init]; NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; AVCaptureDevice *device = nil; AVCaptureDevicePosition camera = AVCaptureDevicePositionBack; // Back or Front for (AVCaptureDevice *d in devices) { device = d; if (d.position == camera) { break; } } NSError *error = nil; AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error]; [self.session addInput:input]; AVCaptureMetadataOutput *output = [AVCaptureMetadataOutput new]; [output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()]; [self.session addOutput:output]; // QR コードのみ //output.metadataObjectTypes = @[AVMetadataObjectTypeQRCode]; // 全部認識させたい場合 // ( // face, // "org.gs1.UPC-E", // "org.iso.Code39", // "org.iso.Code39Mod43", // "org.gs1.EAN-13", // "org.gs1.EAN-8", // "com.intermec.Code93", // "org.iso.Code128", // "org.iso.PDF417", // "org.iso.QRCode", // "org.iso.Aztec" // ) output.metadataObjectTypes = output.availableMetadataObjectTypes; NSLog(@"%@", output.availableMetadataObjectTypes); NSLog(@"%@", output.metadataObjectTypes); [self.session startRunning]; AVCaptureVideoPreviewLayer *preview = [AVCaptureVideoPreviewLayer layerWithSession:self.session]; preview.frame = self.view.bounds; preview.videoGravity = AVLayerVideoGravityResizeAspectFill; [self.view.layer addSublayer:preview]; } - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection { NSLog(@"----"); for (AVMetadataObject *metadata in metadataObjects) { if ([metadata.type isEqualToString:AVMetadataObjectTypeQRCode]) { // 複数の QR があっても1度で読み取れている NSString *qrcode = [(AVMetadataMachineReadableCodeObject *)metadata stringValue]; NSLog(@"%@", qrcode); } else if ([metadata.type isEqualToString:AVMetadataObjectTypeEAN13Code]) { NSString *ean13 = [(AVMetadataMachineReadableCodeObject *)metadata stringValue]; NSLog(@"%@", ean13); } } } @end
か、簡単すぎます・・・。
ZXingObjC を Framework として Xcode プロジェクトに追加して QR コードを読み取る
iOS 開発で QR コードの読み取りが処理が必要になったので、ZXingObjC を使ってみることにしました。
クラスメソッドさんが紹介している「ZXingObjC.xcodeproj をプロジェクトにドロップする方法」でも目的は達成できるのですが、
クリーンビルドすると ZXingObjC 関連ファイルがコンパイルし直されたり、静的解析を走らせたときに ZXingObjC 関連のファイルが引っかかったりするので、Framework としてプロジェクトにリンクすれば解決しそうなのでこの方法を試してみます。
幸い、ZXingObjC は Framework ビルドに対応しているので単にビルドするだけなのですが・・・。
ビルド手順
Github からコードをまるごと取ってきます。執筆時点では 2.1.0 が最新のバージョンとしてタグに登録されているので、そちらを使った方が良いかもしれません。
今回は 2.1.0 以降のコミットを含むリポジトリ全体を取ってくることにしました。
ターミナルで以下のコマンドを入力します。
$ git clone https://github.com/TheLevelUp/ZXingObjC.git
$ cd ZXingObjC
さて、この状態で以下のコマンドを利用してビルドします。-target を指定して iOS Framework としてビルドします。
ちなみにここで xcodebuild -list とすると、ターゲット一覧やビルド構成が表示されます。
$ xcodebuild -list $ xcodebuild -configuration Release -sdk iphoneos -target "iOS Framework" clean build
ビルドが完了したら ZXingObjC.framework が生成されていることを確認します。
$ ls build/Release-iphoneos/
ZXingObjC.framework ZXingObjCHeaders libZXingObjC-iOS.a
この中で必要なのは ZXingObjC.framework のみです。
自分のプロジェクトに追加してみる。
さて、Framework の準備ができたので、プロジェクトにリンクしてみます。
Single View Application として新規プロジェクトを作成します。
今回はプロジェクト名を QRReader としました。
プロジェクトを作成したら、先ほどの ZXingObjC.framework をプロジェクトディレクトリにコピーします。
コピーした ZXingObjC.framework をプロジェクトの Frameworks グループにドラッグ&ドロップします。
すると、以下のように「Link Binary With Libraries」セクションに ZXingObjC.framework が追加されました。
念のため、ビルド設定の「Framework Search Paths」も確認しておきます。
最後に、以下の依存 Framework を追加します (公式の README を参照)。
- AVFoundation.framework
- CoreGraphics.framework
- CoreMedia.framework
- CoreVideo.framework
- ImageIO.framework
- QuartzCore.framework
QR の読み取り
以下のようなビューコントローラを作成して実機でテストします。
#import <ZXingObjC/ZXCapture.h> #import <ZXingObjC/ZXResult.h> #import "ViewController.h" @interface ViewController () <ZXCaptureDelegate> @property (nonatomic, strong) ZXCapture *capture; @end @implementation ViewController - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.capture = [[ZXCapture alloc] init]; // delegate を設定すると start される self.capture.delegate = self; self.capture.camera = self.capture.back; self.capture.layer.frame = self.view.bounds; [self.view.layer addSublayer:self.capture.layer]; } - (void)captureResult:(ZXCapture *)capture result:(ZXResult *)result { NSLog(@"%@", result.text); } @end
実機で動かしてみてログに読み取った QR コードが表示されれば成功です。
Python の difflib で空白文字や数字、大文字小文字の変化を無視する方法
Python の difflib を利用すれば、二つのテキストの差分を表示することができます。
まずは difflib を普通に使ってみる
一般的な使い方は以下のような物です。
# coding: utf-8 import difflib # 元の文字列 a = "11\n\t\t\nheLLo\n12a34" b = "22\n \nHELLO\n56b78" # 行で区切った list を作ります a = a.splitlines() b = b.splitlines() # 変化を表示します d = difflib.Differ() print "\n".join(list(d.compare(a, b)))
結果は以下の通りです
- 11 - - heLLo - 12a34 + 22 + + HELLO + 56b78
テキストのどの行に変化があったかを知ることができました。
大文字小文字や空白文字、数字など、特定の文字を無視したい場合
方法1: 事前に無視したい文字を取り除く
一つ目の方法は差分をとる前に無視したい文字を取り除いておきます。
# coding: utf-8 import difflib import re # 正規表現で数字を探すため # 元の文字列 a = "11\n\t\t\nheLLo\n12a34" b = "22\n \nHELLO\n56b78" # 不要な文字を削除 # まずは空白を削除 a = a.replace("\t", "").replace(" ", "") b = b.replace("\t", "").replace(" ", "") # 続いて、数字を削除 a = re.sub(r"[0-9]+", "", a) b = re.sub(r"[0-9]+", "", b) # すべて小文字に変換 a = a.lower() b = b.lower() # 行で区切った list を作ります a = a.splitlines() b = b.splitlines() # 変化を表示します d = difflib.Differ() print "\n".join(list(d.compare(a, b)))
結果は以下の通りです。
hello - a + b
目的は達成できたのですが、多くのの情報が削られてしまい、文脈の判断ができなくなりました。
方法2: クラス継承によって数字や空白に無関心な文字列クラスを作る
もう一つの方法は、文字列クラスを継承して __eq__ メソッドをオーバーライドします。
これによって、元の文字列を保持したまま、差分対象の比較が可能になります。
# coding: utf-8 import difflib import re # 正規表現で数字を探すため # 元の文字列 a = "11\n\t\t\nheLLo\n12a34" b = "22\n \nHELLO\n56b78" # str クラスを継承した数字や空白に無関心な文字列クラスを作ります。 class InsensitiveStr(str): def __eq__(self, other): # 不要な文字を削除 # まずは空白を削除 a = self.replace("\t", "").replace(" ", "") # a は str になる b = other.replace("\t", "").replace(" ", "") # b は str になる # 続いて、数字を削除 a = re.sub(r"[0-9]+", "", a) b = re.sub(r"[0-9]+", "", b) return a.lower() == b.lower() # str 同士の比較を行う # 行で区切った list を作り、InsensitiveStr でラップします a = [InsensitiveStr(line) for line in a.splitlines()] b = [InsensitiveStr(line) for line in b.splitlines()] # 変化を表示します d = difflib.Differ() print "\n".join(list(d.compare(a, b)))
結果は以下の通りです。
11 heLLo - 12a34 + 56b78
大文字小文字、数字や空白文字は変化していないものとして扱われ、元の行が表示されています。
方法1と違い、数字や空白文字の情報が残るため、文脈の判断がしやすくなりました。
また、方法1では heLLo が hello になってしまっていたのに対し、方法2では元の heLLo のまま表示されるようになりました。