add svelte experiment as well

This commit is contained in:
Damien Elmes 2020-01-06 16:23:55 +10:00
parent a4d37e4014
commit 2037020f2f
20 changed files with 11856 additions and 0 deletions

1
svelte/.eslintignore Normal file
View File

@ -0,0 +1 @@
webpack.config.js

29
svelte/.eslintrc.js Normal file
View File

@ -0,0 +1,29 @@
module.exports = {
extends: ["eslint:recommended"],
parserOptions: {
ecmaVersion: 2019,
sourceType: "module"
},
env: {
es6: true,
browser: true
},
plugins: ["svelte3"],
overrides: [
{
files: ["**/*.svelte"],
processor: "svelte3/svelte3"
}
],
rules: {
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" }
],
"no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" }
]
},
settings: {}
};

6
svelte/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.DS_Store
node_modules
public/bundle.*
dist
es
.vscode

6
svelte/.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"svelteSortOrder" : "styles-scripts-markup",
"svelteStrictMode": true,
"svelteBracketNewLine": true,
"pluginSearchDirs": ["."]
}

38
svelte/Makefile Normal file
View File

@ -0,0 +1,38 @@
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c
.DELETE_ON_ERROR:
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
.SUFFIXES:
$(shell mkdir -p .build)
.PHONY: all
all: run
.PHONY: run
run: build-tslib
tmux new -d -s anki-react "make run-pybackend"
tmux split-window "make run-svelte"
tmux attach
.PHONY: run-pybackend
run-pybackend:
../pyenv/bin/pip install bottle
../pyenv/bin/python ../react/pybackend.py
.PHONY: build-tslib
build-tslib:
(cd ../tslib && make build)
.PHONY: run-svelte
run-svelte: .build/deps
npm run dev
.build/deps: package.json
npm i
@touch $@
.PHONY: svelete-lint
svelte-lint:
npx --no-install svelte-type-checker

2
svelte/README Normal file
View File

@ -0,0 +1,2 @@
This is another experiment, using Svelte instead of React for the UI.
Instructions are the same as ../react/README

11317
svelte/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

41
svelte/package.json Normal file
View File

@ -0,0 +1,41 @@
{
"name": "anki-svelte",
"version": "0.1.0",
"devDependencies": {
"@sveltejs/svelte-virtual-list": "^3.0.1",
"@types/jest": "^24.0.23",
"@typescript-eslint/eslint-plugin": "^2.11.0",
"@typescript-eslint/parser": "^2.11.0",
"cross-env": "^5.2.0",
"css-loader": "^2.1.1",
"eslint": "^6.7.2",
"eslint-loader": "^3.0.3",
"eslint-plugin-svelte3": "^2.7.3",
"jest": "^24.9.0",
"mini-css-extract-plugin": "^0.6.0",
"node-fetch": "^2.6.0",
"prettier": "^1.19.1",
"prettier-plugin-svelte": "^0.7.0",
"serve": "^11.0.0",
"style-loader": "^0.23.1",
"svelte": "^3.16.0",
"svelte-loader": "^2.13.3",
"svelte-spa-router": "^2.0.0",
"svelte-type-checker": "^0.1.4",
"ts-jest": "^24.2.0",
"ts-loader": "^6.2.1",
"typescript": "^3.7.3",
"webpack": "^4.30.0",
"webpack-cli": "^3.3.0",
"webpack-dev-server": "^3.3.1",
"anki": "../tslib"
},
"scripts": {
"build": "cross-env NODE_ENV=production webpack",
"dev": "webpack-dev-server --content-base public"
},
"dependencies": {
"protobufjs": "^6.8.8"
},
"proxy": "http://localhost:5006/"
}

63
svelte/public/global.css Normal file
View File

@ -0,0 +1,63 @@
html, body {
position: relative;
width: 100%;
height: 100%;
background: #f7f7f7;
}
body {
color: #333;
margin: 0;
padding: 8px;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
a {
color: rgb(0,100,200);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:visited {
color: rgb(0,80,160);
}
label {
display: block;
}
input, button, select, textarea {
font-family: inherit;
font-size: inherit;
padding: 0.4em;
margin: 0 0 0.5em 0;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 2px;
}
input:disabled {
color: #ccc;
}
input[type="range"] {
height: 0;
}
button {
color: #333;
background-color: #f4f4f4;
outline: none;
}
button:active {
background-color: #ddd;
}
button:focus {
border-color: #666;
}

17
svelte/public/index.html Normal file
View File

@ -0,0 +1,17 @@
<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width'>
<title>Anki</title>
<link rel='stylesheet' href='global.css'>
<link rel='stylesheet' href='bundle.css'>
<script defer src='bundle.js'></script>
</head>
<body>
</body>
</html>

31
svelte/src/ui/App.svelte Normal file
View File

@ -0,0 +1,31 @@
<style>
/* Style for "active" links; need to mark this :global because the router adds the class directly */
:global(a.active) {
color: red;
}
</style>
<script>
import DeckDueTree from "./DeckDueTree.svelte";
import BrowseScreen from "./BrowseScreen.svelte";
import Router from "svelte-spa-router";
import { link } from "svelte-spa-router";
import active from "svelte-spa-router/active";
const routes = {
// Exact path
"/": DeckDueTree,
"/browse": BrowseScreen
};
</script>
<body>
<nav>
<a href="/" use:link use:active>Decks</a>
<a href="/browse" use:link use:active>Browse</a>
</nav>
<Router {routes} />
</body>

View File

@ -0,0 +1,43 @@
<style>
.row {
/* ensures text is center-aligned */
line-height: 35px;
height: 35px;
overflow: hidden;
background-color: white;
padding-left: 0.5em;
padding-right: 0.5em;
}
.rowAlt {
background-color: #eee;
}
.rowSelected {
background-color: #77f;
}
</style>
<script>
import { Browser } from "anki/dist/browser";
import { onMount, onDestroy } from "svelte";
export let idx = 0;
export let browser = new Browser();
$: cardData = null;
onMount(() => {
browser.getRowData(idx, data => (cardData = data));
});
onDestroy(() => {
browser.cancelRequest(idx);
});
</script>
<div class="row" class:rowAlt="{idx % 2 === 0}" class:rowSelected="{false}">
{#if cardData !== null}{idx}: {cardData}{/if}
</div>

View File

@ -0,0 +1,21 @@
<script>
import BrowseTable from "./BrowseTable";
import BrowseSearchInput from "./BrowseSearchInput";
import { Browser } from "anki/dist/browser";
let browser = new Browser();
async function onSearchEnter(evt) {
console.log(`got ${evt.detail.searchText}`);
browser = new Browser();
const b = new Browser();
await b.search(evt.detail.searchText);
browser = b;
}
</script>
<div>
<BrowseSearchInput on:enter="{onSearchEnter}" />
</div>
<BrowseTable {browser} />

View File

@ -0,0 +1,21 @@
<script>
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
let searchText;
function onKeyUp(e) {
if (e.keyCode === 13) {
console.log(`on key up: e.target.keycode`);
dispatch("enter", { searchText });
}
}
</script>
<input
bind:value="{searchText}"
placeholder="hit enter to search"
on:keyup="{onKeyUp}"
autofocus
/>

View File

@ -0,0 +1,17 @@
<script>
import VirtualList from "@sveltejs/svelte-virtual-list";
import BrowseRow from "./BrowseRow.svelte";
export let browser;
let start, end;
let rows;
$: rows = Array.from(Array(browser.rows()).keys());
</script>
<p>showing {start}-{end} of {rows.length} rows</p>
<VirtualList items="{rows}" let:item bind:start bind:end itemHeight="{35}">
<BrowseRow idx="{item}" {browser} />
</VirtualList>

View File

@ -0,0 +1,70 @@
<style>
button {
border: none;
background: none;
width: 1em;
}
.nodeOuter {
border-bottom: 1px solid #eee;
margin-bottom: 5px;
}
.nodeInner {
padding: 5px;
}
.counts {
float: right;
font-size: smaller;
text-align: right;
}
.new {
color: #000099;
}
.due {
color: #007700;
}
</style>
<script>
import { slide } from "svelte/transition";
import { pb } from "anki/dist/backend";
export let record = new pb.DeckTreeNode();
$: deck = record.names[record.names.length - 1];
$: dueCount = record.reviewCount + record.learnCount;
$: collapsed = record.collapsed;
$: indent = record.names.length > 1 ? 1 : 0;
function onToggleRow() {
collapsed = !collapsed;
}
</script>
<div class="nodeOuter" style="margin-left: {indent * 15}px;">
<div class="nodeInner">
{#if record.children.length > 0}
<button on:click="{onToggleRow}">
{#if collapsed}+{:else}-{/if}
</button>
{:else}
<button></button>
{/if}
{deck}
<div class="counts">
<span class="due">{dueCount}</span>
<br />
<span class="new">{record.newCount}</span>
</div>
</div>
{#if !collapsed}
<div transition:slide|local>
{#each record.children as child, idx (child.deckId)}
<svelte:self record="{child}" />
{/each}
</div>
{/if}
</div>

View File

@ -0,0 +1,27 @@
<style>
.dueTable {
max-width: 50em;
margin-left: auto;
margin-right: auto;
}
</style>
<script>
import DeckDueNode from "./DeckDueNode.svelte";
import { onMount } from "svelte";
import { deckTree } from "anki/dist/backend";
$: records = [];
onMount(async () => {
records = await deckTree();
});
</script>
<div class="outer">
<div class="dueTable">
{#each records as record}
<DeckDueNode {record} />
{/each}
</div>
</div>

14
svelte/src/ui/main.js Normal file
View File

@ -0,0 +1,14 @@
import App from "./App.svelte";
const app = new App({
target: document.body
});
window.onerror = function(e) {
window.alert(`An error occurred: ${e}`);
};
window.onunhandledrejection = function(e) {
window.alert(`An error occurred: ${e.reason}`);
};
export default app;

22
svelte/tsconfig.json Normal file
View File

@ -0,0 +1,22 @@
{
"exclude": ["node_modules", "dist", "es"],
"compilerOptions": {
/* Basic Options */
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
"module": "es6" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
"rootDir": "src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
"strictNullChecks": true /* Enable strict null checks. */,
"strictFunctionTypes": true /* Enable strict checking of function types. */,
"strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */,
"strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
"noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
"alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,
/* Module Resolution Options */
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
"allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
}
}

70
svelte/webpack.config.js Normal file
View File

@ -0,0 +1,70 @@
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const path = require("path");
const mode = process.env.NODE_ENV || "development";
const prod = mode === "production";
module.exports = {
entry: {
bundle: ["./src/ui/main.js"]
},
resolve: {
alias: {
svelte: path.resolve("node_modules", "svelte")
},
extensions: [".mjs", ".js", ".svelte", ".ts"],
mainFields: ["svelte", "browser", "module", "main"]
},
output: {
path: __dirname + "/public",
filename: "[name].js",
chunkFilename: "[name].[id].js"
},
module: {
rules: [
{
test: /\.svelte$/,
use: [
{
loader: "svelte-loader",
options: {
emitCss: true,
hotReload: true
}
},
{ loader: "eslint-loader" }
]
},
{
test: /\.css$/,
use: [
/**
* MiniCssExtractPlugin doesn't support HMR.
* For developing, use 'style-loader' instead.
* */
prod ? MiniCssExtractPlugin.loader : "style-loader",
"css-loader"
]
},
{
test: /\.tsx?$/,
use: ["ts-loader", "eslint-loader"],
exclude: /node_modules/
}
]
},
mode,
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css"
})
],
devtool: prod ? false : "source-map",
devServer: { port: 3000,
proxy: {
'/request': {
target: 'http://localhost:5006'
}
}
}
};