Python×Kivy de Kinoppy（紀伊国屋書店アプリ）を真似してみた

本記事は [個人開発 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/private-development) 14日目の記事です。
そしてQiita初投稿です。はじまして。

Python×Kivyというフレームワークを使用して、Windowsアプリを作成しています。

###背景
 1. 紙本を自炊してPDFにした。スッキリ！
 - さぁ、iPadで読もう
 - 子供にiPad取られた・・・
 - Surface買った！さぁ、Surfaceで読もう
 - iPadと違ってWindows用の良いアプリ無いな
 - Kinoppy（紀伊国屋書店アプリ）良いね！
 - これ今勉強中のKivyで作れるかな。

Kinoppy（紀伊国屋書店アプリ）はとても良いアプリで、おすすめです。

###アプリ概要
本棚とPDFビューワ機能を持っています。
![pdfapp2-min.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280388/3ec52dc9-cc2f-0c50-a54d-ca997fbe513d.gif)


###開発環境
AnacondaNavigator
VSCode
Python3.7
Kivy1.11

###使用ライブラリ等
他人に頼りすぎて沢山ありますが、メインで使用しているものは以下です。
PDFレンダリング：[pdf2image](https://github.com/Belval/pdf2image)
設定ファイル：ruamel.yaml
アイコンフォント：[iconfonts](https://github.com/kivy-garden/garden.iconfonts)

###大変お世話になったサイト
[Kinoppy](https://k-kinoppy.jp/)
[Kivy](https://kivy.org/)
[stackoverflow](https://stackoverflow.com/)
[荒川光線ブログ　本館](https://koh-sen.jp/)
そして
[Qiita:Kivy](https://qiita.com/tags/kivy)

###Todo
結構奥が深くて志半ばですが、以下を目指してます。遠い。。

- [x] PDF読込（ローカルPC）
- [ ] PDF読込（DropBox）
- [x] 本棚管理（本棚追加＆削除）
- [x] D&Dで本の移動（本棚内）
- [ ] D&Dで本の移動（別の本棚）
- [x] ビューワ機能(1or2ページ表示、右or左開き指定)
- [ ] ビューワ機能(自動目次作成)
- [ ] ビューワ機能（検索）

###ソースコード他
以下のKivyのソースコードを２つ抜粋して紹介します。

 1. Scatterによるサムネイルのドラッグアンドドロップ
 - Sleiderの右→左の実現
 - （おまけ）本棚の作り方

####1.Scatterによるサムネイルのドラッグアンドドロップ
サムネイル（本棚に表示される画像）を何秒か掴んだらドラッグアンドドロップします、という事で、画像枠に色を付けます。もしドラッグアンドドロップに入る前に離したら、普通のクリックと見なしてビューワに遷移します。

```Python:Library.kv

<DraggableThumbnail@Scatter>:
    image_name: ''
    image_path: ''
    image: image_id
    shelf_row: None
    shelf_col: None

    do_rotation: False
    do_scale: False
    auto_bring_to_front: True
    do_collide_after_children: True

    background_color: 0,0,0,0
    background_normal: ''

    Image:
        id: image_id
        source: root.image_path
        allow_stretch:True
        keep_ratio: True
        canvas:
            Color:
                rgb: (1, 1, 1)
            Rectangle:
                texture: self.texture
                pos: self.pos
                size: self.size
```


```Python:Library.py

class DraggableThumbnail(Scatter):
    image_name = StringProperty(None)
    image_path = StringProperty(None)
    shelf_row = NumericProperty(None)
    shelf_col = NumericProperty(None)

    state = OptionProperty('released',options=['grabed','released'])
    img_touched = StringProperty(None)
    t = NumericProperty(0)

    def __init__(self,**kwargs):
        super(DraggableThumbnail,self).__init__(**kwargs)
        # ここでClock.schedule_onceしないとサイズ変更出来ない
        Clock.schedule_once(self.drag_after_init)

    def drag_after_init(self, dt):
        self.size_hint = None,None
        self.size = self.image.size
        # ここでドラッグアンドドロップ用の画像枠だけ作る
        with self.canvas:
            self.color = Color(rgba = (1,1,1,0))
            self.frame = Line(rectangle=(0,0,self.image.width,self.image.height))

    def on_state(self,*args):
        # 掴んだら画像枠をグレーにします。
        if self.state == 'grabed':
            self.color.rgba = (0.5,0.5,0.5,1)
        # 離したら元に戻します。
        elif self.state == 'released':
            self.color.rgba =  (1,1,1,0)

    def time_count(self,*args):
        self.t += 0.1

    def on_t(self,*args):
        if self.t >=0.5:
            Clock.unschedule(self.time_count)
            self.t = 0
            self.state = 'grabed'
            self.img_touched = ''

    def on_touch_down(self, touch):
        self.on_state(touch)
        if self.collide_point(*touch.pos):
            Clock.schedule_interval(self.time_count, 0.1)
            self.img_touched = self.image_name

            touch.grab(self)
            return True

    def on_touch_up(self, touch):
        Clock.unschedule(self.time_count)
        self.t = 0

        if touch.grab_current is self:
            if self.img_touched == self.image_name:
                self.img_touched = ''
                if self.state == 'released':
                    # ビューワに遷移
                    app= App.get_running_app()
                    app.root.chgdisp_viewer(self.image_name)
            else:
                # ドロップされたのでサムネイル整列
                app= App.get_running_app()
                app.root.Library.lineup_thumbnail(self,touch)
            touch.ungrab(self)

        self.state = 'released'
        return True

    def on_touch_move(self, touch):
        Clock.unschedule(self.time_count)
        self.t = 0
        if self.state == 'grabed':
            self.pos = touch.x - self.image.width / 2, touch.y - self.image.height / 2
            return True
        else:
            self.img_touched = ''


```



####2.Sleiderの右→左の実現
海外のPDFViewerで不便に思うのが縦書き（右→左）に対応していない事です。Kivyのスライダーは左→右（下→上）は実現できますが、右から左の設定がありません。あとスライドしている時にページ数も欲しかったのでこちらは部品化しました。ソースは[Github](https://github.com/MamiTakata/Kivy-ViewSlider)にアップしてあります。こうやって標準部品をカスタマイズするのかーと勉強になりました。もし良ければソースご覧ください。

![Example.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280388/9b2cc70c-fec0-4a36-8818-730550aa88a2.gif)

####3.（おまけ）本棚の作り方
ちょうどいい本棚画像が無かったので、作りました。この画像の事です。
![shelf2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280388/e3b49d6f-5f7d-01cf-3d73-f3b11e9bbd2d.png)

作り方は[こちらの自分のブログ](https://flat-kids.net/2019/10/06/inkscape0-92-4%e3%81%a7%e6%9c%ac%e6%a3%9a%e3%82%92%e4%bd%9c%e3%82%8b/)にて、プログラムとは関係ありませんし、どこにも需要が無さそうですが。

###最後に
はじめてのQiita投稿とはじめてのAdvent Calendar、１２月１日から自分の担当まで時間が過ぎるのが早いこと！！毎日「あーもうすぐ順番が…」のドキドキが、とても良いモチベーションになりました。このイベントが無かったらここまで仕上がってなかったです。参加出来て良かったです。お読み頂きありがとうございました。