mirror of
https://github.com/nkanaev/yarr.git
synced 2025-05-24 00:33:14 +00:00
459 lines
27 KiB
HTML
459 lines
27 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>yarr!</title>
|
|
<link rel="stylesheet" href="./static/stylesheets/bootstrap.min.css">
|
|
<link rel="stylesheet" href="./static/stylesheets/app.css">
|
|
<link rel="icon shortcut" href="./static/graphicarts/anchor.png">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
</head>
|
|
<body class="theme-light">
|
|
<div id="app" class="d-flex" :class="{'feed-selected': feedSelected !== null, 'item-selected': itemSelected !== null}" v-cloak>
|
|
<!-- feed list -->
|
|
<div id="col-feed-list" class="vh-100 position-relative d-flex flex-column border-right flex-shrink-0" :style="{width: feedListWidth+'px'}">
|
|
<drag :width="feedListWidth" @resize="resizeFeedList"></drag>
|
|
<div class="p-2 toolbar d-flex align-items-center">
|
|
<div class="icon mx-2">{% inline "anchor.svg" %}</div>
|
|
<div class="flex-grow-1"></div>
|
|
<button class="toolbar-item"
|
|
:class="{active: filterSelected == 'unread'}"
|
|
v-b-tooltip.hover.bottom="'Unread'"
|
|
@click="filterSelected = 'unread'">
|
|
<span class="icon">{% inline "circle-full.svg" %}</span>
|
|
</button>
|
|
<button class="toolbar-item"
|
|
:class="{active: filterSelected == 'starred'}"
|
|
v-b-tooltip.hover.bottom="'Starred'"
|
|
@click="filterSelected = 'starred'">
|
|
<span class="icon">{% inline "star-full.svg" %}</span>
|
|
</button>
|
|
<button class="toolbar-item"
|
|
:class="{active: filterSelected == ''}"
|
|
v-b-tooltip.hover.bottom="'All'"
|
|
@click="filterSelected = ''">
|
|
<span class="icon">{% inline "assorted.svg" %}</span>
|
|
</button>
|
|
<div class="flex-grow-1"></div>
|
|
<b-dropdown
|
|
right no-caret lazy variant="link"
|
|
class="settings-dropdown"
|
|
toggle-class="toolbar-item px-2"
|
|
ref="menuDropdown">
|
|
<template v-slot:button-content class="toolbar-item">
|
|
<span class="icon">{% inline "more-horizontal.svg" %}</span>
|
|
</template>
|
|
<b-dropdown-item-button @click="showSettings('create')">
|
|
<span class="icon mr-1">{% inline "plus.svg" %}</span>
|
|
New Feed
|
|
</b-dropdown-item-button>
|
|
<b-dropdown-item-button @click.stop="showSettings('manage')">
|
|
<span class="icon mr-1">{% inline "list.svg" %}</span>
|
|
Manage Feeds
|
|
</b-dropdown-item-button>
|
|
<b-dropdown-divider></b-dropdown-divider>
|
|
<b-dropdown-item-button @click.stop="fetchAllFeeds()">
|
|
<span class="icon mr-1">{% inline "rotate-cw.svg" %}</span>
|
|
Refresh Feeds
|
|
</b-dropdown-item-button>
|
|
|
|
<b-dropdown-divider></b-dropdown-divider>
|
|
|
|
<b-dropdown-header>Refresh</b-dropdown-header>
|
|
<b-dropdown-item-button @click.stop="refreshRate = min" v-for="min in [0, 60]">
|
|
<span class="icon mr-1" :class="{invisible: refreshRate != min}">{% inline "check.svg" %}</span>
|
|
<span v-if="min == 0">Manually</span>
|
|
<span v-if="min == 60">Every hour</span>
|
|
</b-dropdown-item-button>
|
|
|
|
<b-dropdown-divider></b-dropdown-divider>
|
|
|
|
<b-dropdown-header>Sort by</b-dropdown-header>
|
|
<b-dropdown-item-button @click.stop="itemSortNewestFirst=true">
|
|
<span class="icon mr-1" :class="{invisible: !itemSortNewestFirst}">{% inline "check.svg" %}</span>
|
|
Newest First
|
|
</b-dropdown-item-button>
|
|
<b-dropdown-item-button @click="itemSortNewestFirst=false">
|
|
<span class="icon mr-1" :class="{invisible: itemSortNewestFirst}">{% inline "check.svg" %}</span>
|
|
Oldest First
|
|
</b-dropdown-item-button>
|
|
<b-dropdown-divider></b-dropdown-divider>
|
|
<b-dropdown-header>Subscriptions</b-dropdown-header>
|
|
<b-dropdown-form id="opml-import-form" enctype="multipart/form-data">
|
|
<input type="file"
|
|
id="opml-import"
|
|
@change="importOPML"
|
|
name="opml"
|
|
style="opacity: 0; width: 1px; height: 0; position: absolute; z-index: -1;">
|
|
<label class="dropdown-item mb-0 cursor-pointer" for="opml-import">
|
|
<span class="icon mr-1">{% inline "download.svg" %}</span>
|
|
Import
|
|
</label>
|
|
</b-dropdown-form>
|
|
<b-dropdown-item href="./opml/export">
|
|
<span class="icon mr-1">{% inline "upload.svg" %}</span>
|
|
Export
|
|
</b-dropdown-item>
|
|
<b-dropdown-divider></b-dropdown-divider>
|
|
<b-dropdown-item-button @click="showSettings('shortcuts')">
|
|
<span class="icon mr-1">{% inline "help-circle.svg" %}</span>
|
|
Shortcuts
|
|
</b-dropdown-item-button>
|
|
<b-dropdown-divider v-if="authenticated"></b-dropdown-divider>
|
|
<b-dropdown-item-button v-if="authenticated" @click="logout()">
|
|
<span class="icon mr-1">{% inline "log-out.svg" %}</span>
|
|
Log out
|
|
</b-dropdown-item-button>
|
|
</b-dropdown>
|
|
</div>
|
|
<div id="feed-list-scroll" class="p-2 overflow-auto border-top flex-grow-1">
|
|
<label class="selectgroup">
|
|
<input type="radio" name="feed" value="" v-model="feedSelected">
|
|
<div class="selectgroup-label d-flex align-items-center w-100">
|
|
<span class="icon mr-2">{% inline "layers.svg" %}</span>
|
|
<span class="flex-fill text-left text-truncate" v-if="filterSelected=='unread'">All Unread</span>
|
|
<span class="flex-fill text-left text-truncate" v-if="filterSelected=='starred'">All Starred</span>
|
|
<span class="flex-fill text-left text-truncate" v-if="filterSelected==''">All Feeds</span>
|
|
<span class="counter text-right">{{ filteredTotalStats }}</span>
|
|
</div>
|
|
</label>
|
|
<div v-for="folder in foldersWithFeeds">
|
|
<label class="selectgroup mt-1"
|
|
:class="{'d-none': filterSelected
|
|
&& !filteredFolderStats[folder.id]
|
|
&& (!itemSelected || feedsById[itemSelectedDetails.feed_id].folder_id != folder.id)}">
|
|
<input type="radio" name="feed" :value="'folder:'+folder.id" v-model="feedSelected">
|
|
<div class="selectgroup-label d-flex align-items-center w-100" v-if="folder.id">
|
|
<span class="icon mr-2"
|
|
:class="{expanded: folder.is_expanded}"
|
|
@click.prevent="toggleFolderExpanded(folder)">
|
|
{% inline "chevron-right.svg" %}
|
|
</span>
|
|
<span class="flex-fill text-left text-truncate">{{ folder.title }}</span>
|
|
<span class="counter text-right">{{ filteredFolderStats[folder.id] || '' }}</span>
|
|
</div>
|
|
</label>
|
|
<div v-show="!folder.id || folder.is_expanded" class="mt-1" :class="{'pl-3': folder.id}">
|
|
<label class="selectgroup"
|
|
:class="{'d-none': filterSelected
|
|
&& !filteredFeedStats[feed.id]
|
|
&& (!itemSelected || itemSelectedDetails.feed_id != feed.id)}"
|
|
v-for="feed in folder.feeds">
|
|
<input type="radio" name="feed" :value="'feed:'+feed.id" v-model="feedSelected">
|
|
<div class="selectgroup-label d-flex align-items-center w-100">
|
|
<span class="icon mr-2" v-if="!feed.has_icon">{% inline "rss.svg" %}</span>
|
|
<span class="icon mr-2" v-else><img v-lazy="'./api/feeds/'+feed.id+'/icon'" alt=""></span>
|
|
<span class="flex-fill text-left text-truncate">{{ feed.title }}</span>
|
|
<span class="counter text-right">{{ filteredFeedStats[feed.id] || '' }}</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="p-2 toolbar d-flex align-items-center border-top flex-shrink-0" v-if="loading.feeds">
|
|
<span class="icon loading mx-2"></span>
|
|
<span class="text-truncate cursor-default noselect">Refreshing ({{ loading.feeds }} left)</span>
|
|
</div>
|
|
</div>
|
|
<!-- item list -->
|
|
<div id="col-item-list" class="vh-100 position-relative d-flex flex-column border-right flex-shrink-0" :style="{width: itemListWidth+'px'}">
|
|
<drag :width="itemListWidth" @resize="resizeItemList"></drag>
|
|
<div class="px-2 toolbar d-flex align-items-center">
|
|
<button class="toolbar-item mr-2 d-block d-md-none"
|
|
@click="feedSelected = null"
|
|
v-b-tooltip.hover.bottom="'Show Feeds'">
|
|
<span class="icon">{% inline "chevron-left.svg" %}</span>
|
|
</button>
|
|
<div class="input-icon flex-grow-1">
|
|
<span class="icon">{% inline "search.svg" %}</span>
|
|
<!-- id used by keybindings -->
|
|
<input id="searchbar" class="d-block toolbar-search" type="" v-model="itemSearch">
|
|
</div>
|
|
<button class="toolbar-item ml-2"
|
|
@click="markItemsRead()"
|
|
v-if="filterSelected == 'unread'"
|
|
v-b-tooltip.hover.bottom="'Mark All Read'">
|
|
<span class="icon">{% inline "check.svg" %}</span>
|
|
</button>
|
|
</div>
|
|
<div id="item-list-scroll" class="p-2 overflow-auto border-top flex-grow-1" v-scroll="loadMoreItems" ref="itemlist">
|
|
<label v-for="item in items" :key="item.id"
|
|
class="selectgroup">
|
|
<input type="radio" name="item" :value="item.id" v-model="itemSelected">
|
|
<div class="selectgroup-label d-flex flex-column">
|
|
<div style="line-height: 1; opacity: .7; margin-bottom: .1rem;" class="d-flex align-items-center">
|
|
<transition name="indicator">
|
|
<span class="icon icon-small mr-1" v-if="item.status=='unread'">{% inline "circle-full.svg" %}</span>
|
|
<span class="icon icon-small mr-1" v-if="item.status=='starred'">{% inline "star-full.svg" %}</span>
|
|
</transition>
|
|
<small class="flex-fill text-truncate mr-1">
|
|
{{ feedsById[item.feed_id].title }}
|
|
</small>
|
|
<small class="flex-shrink-0"><relative-time :val="item.date"/></small>
|
|
</div>
|
|
<div>{{ item.title || 'untitled' }}</div>
|
|
</div>
|
|
</label>
|
|
<button class="btn btn-link btn-block loading my-3" v-if="itemsPage.cur < itemsPage.num"></button>
|
|
</div>
|
|
</div>
|
|
<!-- item show -->
|
|
<div id="col-item" class="vh-100 d-flex flex-column w-100" style="min-width: 0;">
|
|
<div class="toolbar px-2 d-flex align-items-center" v-if="itemSelected">
|
|
<button class="toolbar-item"
|
|
@click="toggleItemStarred(itemSelectedDetails)"
|
|
v-b-tooltip.hover.bottom="'Mark Starred'">
|
|
<span class="icon" v-if="itemSelectedDetails.status=='starred'" >{% inline "star-full.svg" %}</span>
|
|
<span class="icon" v-else-if="itemSelectedDetails.status!='starred'" >{% inline "star.svg" %}</span>
|
|
</button>
|
|
<button class="toolbar-item"
|
|
:disabled="itemSelectedDetails.status=='starred'"
|
|
v-b-tooltip.hover.bottom="'Mark Unread'"
|
|
@click="toggleItemRead(itemSelectedDetails)">
|
|
<span class="icon" v-if="itemSelectedDetails.status=='unread'">{% inline "circle-full.svg" %}</span>
|
|
<span class="icon" v-if="itemSelectedDetails.status!='unread'">{% inline "circle.svg" %}</span>
|
|
</button>
|
|
<a class="toolbar-item" id="content-appearance" v-b-tooltip.hover.bottom="'Appearance'" tabindex="0">
|
|
<span class="icon">{% inline "sliders.svg" %}</span>
|
|
</a>
|
|
<button class="toolbar-item"
|
|
:class="{active: itemSelectedReadability}"
|
|
@click="getReadable(itemSelectedDetails)"
|
|
v-b-tooltip.hover.bottom="'Read Here'">
|
|
<span class="icon" :class="{'icon-loading': loading.readability}">{% inline "book-open.svg" %}</span>
|
|
</button>
|
|
<a class="toolbar-item" :href="itemSelectedDetails.link" target="_blank" v-b-tooltip.hover.bottom="'Open Link'">
|
|
<span class="icon">{% inline "external-link.svg" %}</span>
|
|
</a>
|
|
<b-popover target="content-appearance" triggers="focus" placement="bottom">
|
|
<div class="p-1" style="width: 200px;">
|
|
<div class="d-flex">
|
|
<label class="themepicker">
|
|
<input type="radio" name="settingsTheme" value="light" v-model="theme.name">
|
|
<div class="themepicker-label appearance-option"></div>
|
|
</label>
|
|
<label class="themepicker">
|
|
<input type="radio" name="settingsTheme" value="sepia" v-model="theme.name">
|
|
<div class="themepicker-label appearance-option"></div>
|
|
</label>
|
|
<label class="themepicker">
|
|
<input type="radio" name="settingsTheme" value="night" v-model="theme.name">
|
|
<div class="themepicker-label appearance-option"></div>
|
|
</label>
|
|
</div>
|
|
<div class="mt-2">
|
|
<label class="selectgroup">
|
|
<input type="radio" name="font" value="" v-model="theme.font">
|
|
<div class="selectgroup-label appearance-option">
|
|
System Default
|
|
</div>
|
|
</label>
|
|
<label class="selectgroup" v-for="f in fonts" :key="f">
|
|
<input type="radio" name="font" :value="f" v-model="theme.font">
|
|
<div class="selectgroup-label appearance-option":style="{'font-family': f}">
|
|
{{ f }}
|
|
</div>
|
|
</label>
|
|
</div>
|
|
<div class="btn-group d-flex mt-2">
|
|
<button class="btn btn-outline appearance-option"
|
|
style="font-size: 0.8rem" @click="incrFont(-1)">A</button>
|
|
<button class="btn btn-outline appearance-option"
|
|
style="font-size: 1.2rem" @click="incrFont(1)">A</button>
|
|
</div>
|
|
</div>
|
|
</b-popover>
|
|
<div class="flex-grow-1"></div>
|
|
<button class="toolbar-item" @click="itemSelected=null" v-b-tooltip.hover.bottom="'Close Article'">
|
|
<span class="icon">{% inline "x.svg" %}</span>
|
|
</button>
|
|
</div>
|
|
<div v-if="itemSelected"
|
|
ref="content"
|
|
class="content px-4 pt-3 pb-5 border-top overflow-auto"
|
|
:style="{'font-family': theme.font, 'font-size': theme.size + 'rem'}">
|
|
<h1><b>{{ itemSelectedDetails.title }}</b></h1>
|
|
<div class="text-muted">
|
|
<div>{{ feedsById[itemSelectedDetails.feed_id].title }}</div>
|
|
<time>{{ formatDate(itemSelectedDetails.date) }}</time>
|
|
</div>
|
|
<hr>
|
|
<audio class="w-100" controls v-if="itemSelectedDetails.podcast_url" :src="itemSelectedDetails.podcast_url"></audio>
|
|
<div v-html="itemSelectedContent"></div>
|
|
</div>
|
|
</div>
|
|
<b-modal id="settings-modal" hide-header hide-footer lazy>
|
|
<button class="btn btn-link outline-none float-right p-2 mr-n2 mt-n2" style="line-height: 1" @click="$bvModal.hide('settings-modal')">
|
|
<span class="icon">{% inline "x.svg" %}</span>
|
|
</button>
|
|
<div v-if="settings=='create'">
|
|
<p class="cursor-default"><b>New Feed</b></p>
|
|
<form action="" @submit.prevent="createFeed(event)" class="mt-4">
|
|
<label for="feed-url">URL</label>
|
|
<input id="feed-url" name="url" type="url" class="form-control" required autocomplete="off" :readonly="feedNewChoice.length > 0">
|
|
<label for="feed-folder" class="mt-3 d-block">
|
|
Folder
|
|
<a href="#" class="float-right text-decoration-none" @click.prevent="createNewFeedFolder()">new folder</a>
|
|
</label>
|
|
<select class="form-control" id="feed-folder" name="folder_id" ref="newFeedFolder">
|
|
<option value="">---</option>
|
|
<option :value="folder.id" v-for="folder in folders">{{ folder.title }}</option>
|
|
</select>
|
|
<div class="mt-4" v-if="feedNewChoice.length">
|
|
<p class="mb-2">
|
|
Multiple feeds found. Choose one below:
|
|
<a href="#" class="float-right text-decoration-none" @click.prevent="resetFeedChoice()">cancel</a>
|
|
</p>
|
|
<label class="selectgroup" v-for="choice in feedNewChoice">
|
|
<input type="radio" name="feedToAdd" :value="choice.url" v-model="feedNewChoiceSelected">
|
|
<div class="selectgroup-label">
|
|
<div class="text-truncate">{{ choice.title }}</div>
|
|
<div class="text-truncate" :class="{light: choice.title}">{{ choice.url }}</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
<button class="btn btn-block btn-default mt-3" :class="{loading: loading.newfeed}" type="submit">Add</button>
|
|
</form>
|
|
</div>
|
|
<div v-else-if="settings=='manage'">
|
|
<p class="cursor-default"><b>Manage Feeds</b></p>
|
|
<div v-for="folder in foldersWithFeeds" class="mt-4" :key="folder.id">
|
|
<div class="list-row d-flex align-items-center">
|
|
<div class="w-100 text-truncate" v-if="folder.id">
|
|
<span class="icon mr-2">{% inline "folder.svg" %}</span>
|
|
{{ folder.title }}
|
|
</div>
|
|
<div class="flex-shrink-0" v-if="folder.id">
|
|
<b-dropdown right no-caret lazy variant="link" class="settings-dropdown" toggle-class="text-decoration-none">
|
|
<template v-slot:button-content>
|
|
<span class="icon">{% inline "more-vertical.svg" %}</span>
|
|
</template>
|
|
<b-dropdown-header>{{ folder.title }}</b-dropdown-header>
|
|
<b-dropdown-item @click.prevent="renameFolder(folder)">Rename</b-dropdown-item>
|
|
<b-dropdown-divider></b-dropdown-divider>
|
|
<b-dropdown-item class="dropdown-danger"
|
|
@click.prevent="deleteFolder(folder)">
|
|
Delete
|
|
</b-dropdown-item>
|
|
</b-dropdown>
|
|
</div>
|
|
</div>
|
|
<div v-for="feed in folder.feeds" class="list-row d-flex align-items-center" :key="feed.id">
|
|
<div class="w-100 text-truncate">
|
|
<span class="icon mr-2" v-if="!feed.has_icon">{% inline "rss.svg" %}</span>
|
|
<span class="icon mr-2" v-else><img v-lazy="'./api/feeds/'+feed.id+'/icon'" alt=""></span>
|
|
{{ feed.title }}
|
|
</div>
|
|
<span class="icon flex-shrink-0 mx-2"
|
|
v-b-tooltip.hover.top="feed_errors[feed.id]"
|
|
v-if="feed_errors[feed.id]">
|
|
{% inline "alert-circle.svg" %}
|
|
</span>
|
|
<div class="flex-shrink-0">
|
|
<b-dropdown right no-caret lazy variant="link" class="settings-dropdown" toggle-class="text-decoration-none">
|
|
<template v-slot:button-content>
|
|
<span class="icon">{% inline "more-vertical.svg" %}</span>
|
|
</template>
|
|
<b-dropdown-header>{{ feed.title }}</b-dropdown-header>
|
|
<b-dropdown-item :href="feed.link" target="_blank" v-if="feed.link">Visit Website</b-dropdown-item>
|
|
<b-dropdown-divider v-if="feed.link"></b-dropdown-divider>
|
|
<b-dropdown-item @click.prevent="renameFeed(feed)">Rename</b-dropdown-item>
|
|
<b-dropdown-divider v-if="folders.length"></b-dropdown-divider>
|
|
<b-dropdown-header v-if="folders.length">Move to...</b-dropdown-header>
|
|
<b-dropdown-item @click="moveFeed(feed, null)" v-if="feed.folder_id">
|
|
---
|
|
</b-dropdown-item>
|
|
<b-dropdown-item-button
|
|
v-if="folder.id != feed.folder_id"
|
|
v-for="folder in folders"
|
|
@click="moveFeed(feed, folder)">
|
|
<span class="icon mr-1">{% inline "folder.svg" %}</span>
|
|
{{ folder.title }}
|
|
</b-dropdown-item-button>
|
|
<b-dropdown-item-button @click="moveFeedToNewFolder(feed)">
|
|
<span class="text-muted icon mr-1">{% inline "plus.svg" %}</span>
|
|
<span class="text-muted">New Folder</span>
|
|
</b-dropdown-item-button>
|
|
<b-dropdown-divider></b-dropdown-divider>
|
|
<b-dropdown-item class="dropdown-danger"
|
|
@click.prevent="deleteFeed(feed)">
|
|
Delete
|
|
</b-dropdown-item>
|
|
</b-dropdown>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else-if="settings=='shortcuts'">
|
|
<p class="cursor-default"><b>Keyboard Shortcuts</b></p>
|
|
|
|
<table class="table table-borderless table-sm table-compact m-0">
|
|
<tr>
|
|
<td>
|
|
<kbd>1</kbd> /
|
|
<kbd>2</kbd> /
|
|
<kbd>3</kbd>
|
|
</td>
|
|
<td>show unread / starred / all feeds</td>
|
|
</tr>
|
|
<tr>
|
|
<td><kbd>/</kbd></td>
|
|
<td>focus the search bar</td>
|
|
</tr>
|
|
|
|
<tr><td colspan=2> </td></tr>
|
|
<tr>
|
|
<td>
|
|
<kbd>j</kbd>
|
|
<kbd>k</kbd>
|
|
</td>
|
|
<td>next / prev article</td>
|
|
</tr>
|
|
<tr>
|
|
<td>
|
|
<kbd>l</kbd>
|
|
<kbd>h</kbd>
|
|
</td>
|
|
<td>next / prev feed</td>
|
|
</tr>
|
|
|
|
<tr><td colspan=2> </td></tr>
|
|
<tr>
|
|
<td><kbd>R</kbd></td>
|
|
<td>mark all articles as read</td>
|
|
</tr>
|
|
<tr>
|
|
<td>
|
|
<kbd>r</kbd>
|
|
</td>
|
|
<td>toggle an article as read / unread</td>
|
|
</tr>
|
|
<tr>
|
|
<td><kbd>s</kbd></td>
|
|
<td>toggle an article as starred / unstarred</td>
|
|
</tr>
|
|
<tr>
|
|
<td><kbd>o</kbd></td>
|
|
<td>open an article's link</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- polyfill -->
|
|
<script src="./static/javascripts/fetch.umd.js"></script>
|
|
<script src="./static/javascripts/url-polyfill.min.js"></script>
|
|
<!-- external -->
|
|
<script src="./static/javascripts/vue.min.js"></script>
|
|
<script src="./static/javascripts/vue-lazyload.js"></script>
|
|
<script src="./static/javascripts/popper.min.js"></script>
|
|
<script src="./static/javascripts/bootstrap-vue.min.js"></script>
|
|
<script src="./static/javascripts/Readability.min.js"></script>
|
|
<script src="./static/javascripts/purify.min.js"></script>
|
|
<!-- internal -->
|
|
<script src="./static/javascripts/api.js"></script>
|
|
<script src="./static/javascripts/app.js"></script>
|
|
<script src="./static/javascripts/keybindings.js"></script>
|
|
</body>
|
|
</html>
|