MarinDeck（TweetDeck）で画像のサムネのアスペクト比を元画像通りにするカスタムCSS, jsを書いた

# 事の発端

私の友人にMarinDeckを勧めたら下の画像のように聞かれた

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714294/0d0c40b5-5e32-3fb0-49bc-9d31a516df40.png" width=75%/>

まあ、確かにそうやね
私も結構MarinDeckを使ってて不便だなとは思ってたからこの際解決しようということでカスタムCSS, jsを書いた

---

MairnDeckを知らない人は下のリンク先を見るなりググるなりしてね

https://hisubway.online/marindeck/

# 大変なことが発覚した（2022/4/28 00:03追記）

Android版のMarinDeckには「Display images in the original aspect ratio」の項目があって、この記事でできるようになることは公式に設定が可能なのでAndroidをお使いの方々はそっちを使うのがいいと思います

私みたいにiPadで使ってるぜって方々はこの記事が役に立つかも？

# やったこと

## 取り敢えず調査

先の画像にあった「TweetDeckではサムネの縦長表示ができる」というのは多分「Better TweetDeckによってできる」ということなのでGithubからBetter TweetDeckのソースコードを読んだり、実際にブラウザ拡張機能を入れて開発者モードでCSSを見たりしてどうやって実現してるのかを確認した

https://github.com/eramdam/BetterTweetDeck

https://chrome.google.com/webstore/detail/better-tweetdeck/micblkellenpbfapmcpcfhcoeohhnpob

ざっと見たところ以下のCSSが書かれていることがわかった

```css
.media-size-medium.btd-aspect-ratio-thumbnail, .media-size-large.btd-aspect-ratio-thumbnail {
    height: 0 !important;
    padding-top: min(var(--btd-max-og-ratio-height), calc(var(--btd-thumb-height) / var(--btd-thumb-width) * 100%));
}
```

画像の高さを0にして`padding-top`で画像表示領域を確保しているらしい
`--btd-max-og-ratio-height`は最大の高さで、`--btd-thumb-height`, `--btd-thumb-width`はそれぞれ画像の縦と横の大きさでそれらを使って元画像と同じアスペクト比で幅いっぱいに広げてるらしい

jsの方は以下のようになっていた（一部抜粋）

```js
quotedMediaNode.classList.add('btd-aspect-ratio-thumbnail');
quotedMediaNode.style.setProperty('--btd-thumb-height', quotedSizeObject.height.toString());
quotedMediaNode.style.setProperty('--btd-thumb-width', quotedSizeObject.width.toString());
```

どうやら画像に対して上のCSSを適用するようにclassを追加して、上で上げた画像の縦横の大きさパラメータをスタイルシートとして渡しているらしい

まあ、同じような動作をするように書けば動くでしょう

## カスタムCSSを書く

カスタムCSSは以下のようにした

```css
.aspect-ratio {
  height: 0 !important;
  padding-top: min(50vh, calc(var(--thumb-height) / var(--thumb-width) * var(--media-width)));
}
```

やってることはBetter TweetDeckのCSSと同じ
（クラス名とかの命名がダサいのは気にしないでください）

## カスタムjsを書く

カスタムjsは以下のようにした

```js

const aspect = () => {
    const mediaItems = document.getElementsByClassName('js-media-image-link');
    for (const mediaItem of mediaItems) {
        const imageURL = mediaItem.style.getPropertyValue('background-image').replace(/^url\(["']?/, '').replace(/["']?\)$/, '');
        const image = new Image()
        image.src = imageURL;

        if(mediaItem.childElementCount == 0) {
            mediaItem.classList.add('aspect-ratio');
            mediaItem.style.setProperty('--thumb-height', image.naturalHeight);
            mediaItem.style.setProperty('--thumb-width', image.naturalWidth);
            mediaItem.style.setProperty('--media-width', mediaItem.clientWidth + 'px');

            // 複数画像のときは圧縮しないで全部縦に並べる
            if (mediaItem.classList.contains('media-image')) {
                mediaItem.classList.remove('pin-all');
                mediaItem.parentElement.classList.remove('position-rel');
                mediaItem.style.setProperty('border-radius', '14px');
                mediaItem.parentElement.style.setProperty('margin-bottom', '8px');
                mediaItem.parentElement.parentElement.classList.remove('media-grid-2', 'media-grid-3', 'media-grid-4');
                mediaItem.parentElement.parentElement.parentElement.style.setProperty('height', 'auto', 'important');
            }
        }else {
            mediaItem.classList.remove('aspect-ratio');
        }
    }
}

const body = document.body;
const observer = new MutationObserver(() => {
    aspect()
});
const config = {
    childList: true,
    attributes: false,
    characterData: false,
    subtree: true
}
observer.observe(body, config);
aspect();

```

### 2022/4/28 00:03追記

ツイート単体を表示した際に画像が見えない状態になったので修正

### 2022/4/28 09:44追記

複数の画像を含んだツイートでも全ての画像が大きく表示されるようにした

---

こっちもやってることはBetter TweetDeckと同じだけどところどころ解説していく

```js
const imageURL = mediaItem.style.getPropertyValue('background-image').replace(/^url\(["']?/, '').replace(/["']?\)$/, '');
const image = new Image()
image.src = imageURL;
```

サムネ画像は`background-image`に設定されているっぽいので正規表現を使ってURLの文字列を取り出して、画像オブジェクトとして持つ

```js
if(mediaItem.childElementCount == 0) {
     mediaItem.classList.add('aspect-ratio');
     mediaItem.style.setProperty('--thumb-height', image.naturalHeight);
     mediaItem.style.setProperty('--thumb-width', image.naturalWidth);
}else {
    mediaItem.classList.remove('aspect-ratio');
}
```

~~ここでやってるのはBetter TweetDeckと全く同じ~~
クラスを付与して先に取得した画像オブジェクトから縦横の大きさを取得してスタイルで保持

**2022/4/28 00:03追記**
ツイート単体を表示しているときは`media-item`のクラスを持つタグの`background-image`ではなくそのタグの子要素としてimgタグが画像を表示しているため、子要素が存在する場合は`aspect-ratio`クラスを剥奪して画像がちゃんと表示されるようにした

**2022/4/28 09:43追記**
複数の画像付きツイートの場合は圧縮表示を無効化して全て大きく表示するようにした

---

```js
const body = document.body;
const observer = new MutationObserver(() => {
    aspect()
});
const config = {
    childList: true,
    attributes: false,
    characterData: false,
    subtree: true
}
observer.observe(body, config);
aspect();
```

あとはオブザーバーでbody全体を子要素も含めて監視させて変化があったとき（新たなツイートが流れてきたときとか）に上で書いた処理をやらせる

## MarinDeckに適用

あとはMarinDeckの設定からカスタムCSS, jsをそれぞれ入力してリロードすれば完成

# おわりに

 - MarinDeckはスマホ・タブレットでTweetDeckが使える素晴らしいアプリ
 - ただ、カスタムCSS, jsの書き方というかリファレンスみたいなやつを出してくれるとありがたい（これをMarinDeckに対して言うのは違う気がする）
 - Android版には公式でこの機能はあるからAndroid版を使ってる人たちはアプリの設定からこれを使ったほうが楽（MarinDeckのテーマを作ってる人の引用リツイートで気づいた）