wip
This commit is contained in:
BIN
bookerly.woff2
Normal file
BIN
bookerly.woff2
Normal file
Binary file not shown.
130
index.html
130
index.html
@@ -1,15 +1,131 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1">
|
||||||
|
|
||||||
<title>Dandelion</title>
|
<title>Dandelion</title>
|
||||||
<script src="main.js"></script>
|
<script type="module" src="main.js"></script>
|
||||||
</head>
|
<script src="marked.min.js"></script>
|
||||||
<body>
|
|
||||||
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Virgil';
|
||||||
|
/* Define the name you want to reference your font with */
|
||||||
|
src: url('./virgil.woff2') format('woff2');
|
||||||
|
/* URL to the font file and format */
|
||||||
|
font-weight: normal;
|
||||||
|
/* Define the weight of the font */
|
||||||
|
font-style: normal;
|
||||||
|
/* Define the style of the font */
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Bookerly';
|
||||||
|
/* Define the name you want to reference your font with */
|
||||||
|
src: url('./bookerly.woff2') format('woff2');
|
||||||
|
/* URL to the font file and format */
|
||||||
|
font-weight: normal;
|
||||||
|
/* Define the weight of the font */
|
||||||
|
font-style: normal;
|
||||||
|
/* Define the style of the font */
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Bookerly, sans-serif;
|
||||||
|
color: rgb(202, 208, 211);
|
||||||
|
background-color: black;
|
||||||
|
/* Use the font with a fallback */
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border-color: rgb(128, 128, 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
#textarea_post {
|
||||||
|
font-size: medium;
|
||||||
|
font-family: Bookerly;
|
||||||
|
background-color: rgb(0, 0, 0);
|
||||||
|
color: rgb(202, 208, 211);
|
||||||
|
width: 100%;
|
||||||
|
/* Make the textarea take up 100% of the body's width */
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* Include padding and border in the element's total width and height */
|
||||||
|
padding: 10px;
|
||||||
|
/* Optional padding for the inside of the textarea */
|
||||||
|
border: 1px solid rgb(132, 136, 138);
|
||||||
|
/* Optional border for the textarea */
|
||||||
|
resize: vertical;
|
||||||
|
/* Allows the textarea to be resized vertically only */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.flex-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 0px;
|
||||||
|
/* Add some padding around the flex container */
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
max-width: 800px;
|
||||||
|
/* Your preferred max width for the content */
|
||||||
|
flex: 1;
|
||||||
|
/* Shorthand for flex-grow, flex-shrink and flex-basis */
|
||||||
|
min-width: 300px;
|
||||||
|
/* Minimum width the content can shrink to */
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 0 10px rgb(60, 60, 60);
|
||||||
|
text-align: left;
|
||||||
|
overflow-x: hidden;
|
||||||
|
/* Hide horizontal overflow inside the flex container */
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed {
|
||||||
|
max-width: 800px;
|
||||||
|
border-color: red;
|
||||||
|
border: 1px, solid;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#log {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgb(29, 155, 240);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex-container">
|
||||||
|
<div class="content">
|
||||||
<div id="status"></div>
|
<div id="status"></div>
|
||||||
<div id="log" style="margin-bottom: 100px"></div>
|
<div id="log" style="margin-bottom: 20px; height:100px;"></div>
|
||||||
<textarea cols="40" rows="8" id="textarea_post"></textarea>
|
<input type="button" value="font1" id="button_font1" />
|
||||||
|
<input type="button" value="font2" id="button_font2" />
|
||||||
|
|
||||||
|
<textarea cols="60" rows="8" id="textarea_post"></textarea>
|
||||||
|
<div class="button">
|
||||||
<input type="button" value="post" id="button_post" />
|
<input type="button" value="post" id="button_post" />
|
||||||
|
</div>
|
||||||
<div id="content"></div>
|
<div id="content"></div>
|
||||||
</body>
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
112
main.go
112
main.go
@@ -1,14 +1,46 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LoggingHandler is a custom http.Handler that logs requests and delegates them to the underlying handler.
|
var upgrader = websocket.Upgrader{
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
return true // Note: In production, you'd want to check the origin.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleWebSocket handles WebSocket requests from the peer.
|
||||||
|
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("Websocket connection!", r.RemoteAddr)
|
||||||
|
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Upgrade error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
mt, message, err := conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Read error:", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
log.Printf("recv: %s", message)
|
||||||
|
err = conn.WriteMessage(mt, message)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Write error:", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggingHandler logs requests and delegates them to the underlying handler.
|
||||||
type LoggingHandler struct {
|
type LoggingHandler struct {
|
||||||
handler http.Handler
|
handler http.Handler
|
||||||
}
|
}
|
||||||
@@ -18,75 +50,25 @@ func (lh *LoggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
lh.handler.ServeHTTP(w, r)
|
lh.handler.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAddressString(portNumber int) string {
|
|
||||||
return (":" + strconv.Itoa(portNumber))
|
|
||||||
}
|
|
||||||
|
|
||||||
func portIsInUse(port int) bool {
|
|
||||||
addr := fmt.Sprintf(":%d", port)
|
|
||||||
conn, err := net.Listen("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
conn.Close()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
dir := "./"
|
dir := "./"
|
||||||
startPort := 8000
|
port := 9000
|
||||||
const maxAttempts = 10
|
|
||||||
|
|
||||||
fmt.Println("Starting...")
|
|
||||||
|
|
||||||
// Configure TLS with the self-signed certificate and private key
|
|
||||||
// tlsConfig := &tls.Config{
|
|
||||||
// MinVersion: tls.VersionTLS12,
|
|
||||||
// PreferServerCipherSuites: true,
|
|
||||||
// InsecureSkipVerify: true,
|
|
||||||
// Certificates: make([]tls.Certificate, 1),
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Load the certificate and private key
|
|
||||||
// cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem")
|
|
||||||
// if err != nil {
|
|
||||||
// log.Fatalf("Failed to load certificate and key: %v", err)
|
|
||||||
// }
|
|
||||||
// tlsConfig.Certificates[0] = cert
|
|
||||||
|
|
||||||
port := startPort
|
|
||||||
for attempts := 0; attempts < maxAttempts; attempts++ {
|
|
||||||
fmt.Printf("Trying port %d", port)
|
|
||||||
|
|
||||||
if portIsInUse(port) {
|
|
||||||
fmt.Println("...port in use!")
|
|
||||||
port++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the port is not in use, bind your server here
|
|
||||||
// For example:
|
|
||||||
addr := ":" + strconv.Itoa(port)
|
addr := ":" + strconv.Itoa(port)
|
||||||
log.Printf("\nServing %s on %s using HTTP/2...", dir, addr)
|
log.Printf("Starting server on %s", addr)
|
||||||
|
|
||||||
// Configure the HTTP/2 server
|
// Set up file server and WebSocket endpoint
|
||||||
|
fs := http.FileServer(http.Dir(dir))
|
||||||
|
loggingHandler := &LoggingHandler{handler: fs}
|
||||||
|
http.Handle("/", loggingHandler)
|
||||||
|
http.HandleFunc("/ws", handleWebSocket)
|
||||||
|
|
||||||
|
// Configure and start the HTTP server
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
Handler: &LoggingHandler{http.FileServer(http.Dir(dir))},
|
Handler: nil, // nil uses the default ServeMux, which we configured above
|
||||||
// TLSConfig: tlsConfig,
|
|
||||||
}
|
|
||||||
fmt.Println("Configured...")
|
|
||||||
|
|
||||||
fmt.Println("Serving...")
|
|
||||||
// Start the server
|
|
||||||
server.ListenAndServe()
|
|
||||||
if err := server.ListenAndServeTLS("", ""); err != nil {
|
|
||||||
log.Fatalf("Server failed: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Server started on port:", port)
|
log.Printf("Server is configured and serving on port %d...", port)
|
||||||
|
log.Fatal(server.ListenAndServe())
|
||||||
}
|
|
||||||
log.Fatalf("Could not find an open port after %d attempts", maxAttempts)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
6
marked.min.js
vendored
Normal file
6
marked.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
75
ranting.md
Normal file
75
ranting.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
|
||||||
|
They can delete your data and the value of your data [like this](https://twitter.com/shahidkamal/status/1720868040680575414)
|
||||||
|
|
||||||
|
|
||||||
|
# 0. Some ranting and a blueprint for the future.
|
||||||
|
I'm going to outline a few things that I think are really bad patterns wiht current software and creators ecosystems. Them I'm gonna tell you what I think we should do about it.
|
||||||
|
|
||||||
|
# 1. You don't own anything
|
||||||
|
For a long time, i've been increasingly frustrated with software that I have to use on a daily basis. This is due to a number of factors:
|
||||||
|
|
||||||
|
|
||||||
|
## Software in the ancient times
|
||||||
|
In the prehistoric period of owning computers and software, you'd buy some software and you'd use it on on your computer. The data you created was saved in files on floppy and then hard drives.
|
||||||
|
|
||||||
|
This was a very plesant state of affairs. As long as your computer kept working, you could use the software indefinitely and get the same value from it.
|
||||||
|
|
||||||
|
There were significatn downsides to storing data locally, and it was a fairly common thing for people to completely lose important information, reports, source code etc. if it wasn't backed up somewhere else, which was not a common thing for most people to do.
|
||||||
|
|
||||||
|
## Modern day
|
||||||
|
Today, almost no software works this way. The sofware we use is either a website where the software is delivered when you access the website, and almost never works offline, or it's "software as a service" where in place of owning a licence of the software indefinitely, you now need to pay monthly forever for the same thing.
|
||||||
|
|
||||||
|
Your data is stored on a remote server that a company owns, and often, if you don't meet the requirements that company has for using their service, you can lose all access to that data. You also have no idea what's being done with your data. Althogh companies publish privacy policies, the only time you'd discover if whey they said was actually true would be if they got investigated. This almost never happens, so regardless of their promises, they might be just allowing their employees to [watch you via your smart camera][ring].
|
||||||
|
|
||||||
|
## Why did this happen?
|
||||||
|
For websites, it sort of makes sense that this happened. Websites used to be rendered on a server and sent to your web browser, so it kind of made sense that all the data would be held on the server. For things like Gmail, the server computers were much more powerful than your client running a web browser, so they could do things like index all your mail and give you instant search. This was super valuable in an age when email was the primary form of electronic information exchange, and computers and hard drives were relatively slow.
|
||||||
|
|
||||||
|
Today even your phone is incredibly fast and could search hundreds of megs of text in milliseconds and web pages are no longer dumb clients to complex servers, in fact it's pretty much the other way around! Web apps are increasingly complex and tend to pull data via an API on the server. There's been a recent swing back to server rendered pages, but given today's web APIs, it's completely possible to delived a completely offline experience and do pretty much everything locally.
|
||||||
|
|
||||||
|
For downloadable software, the pitch was that instead of getting a single version of the software, you would now get all updates as long as you continued to subscribe, and that ongoing paymets would fund ongoing development and innovation.
|
||||||
|
|
||||||
|
If you tended to upgrade to new versions, that *seems* to be pretty much equivalent, but there are a couple of hidden problems that eventually came to dominate this model. Firstly, for things like Photoshop, the pace of innovation pretty much plataued. Yes there are aamzing new AI tools in the most recent versions of PS, but if you don't use those new tools and are just a traditional user, the appliaction hasn't really changed in a decade. Secondly, Even if the price were equivalent, if you stop paying, then you lose access. So that's not equivalent at all.
|
||||||
|
|
||||||
|
Ultimately, this wasn't about delivering software innovation. It was about maximizing profits for the companies that make the software. If you need to keep subscribing forever, they can turn what used to be a one-time $200 purchase, into a lifetime of payment for new features, regardless if you want them or not.
|
||||||
|
|
||||||
|
Over time, this meant that users don't own anything. They dont own their software and they don't own their data.
|
||||||
|
|
||||||
|
## The shifting sands of UX
|
||||||
|
This new software model also creates what I call the Shifting Sands of UX. The new software model requires constant innovation to justify the ongoing subscription cost to users, which means in turn that there are people who's job it is to come into work on a Monday morning and to "innoavte", whether the software needs it or not. The result is that every piece of software that I use now regularly changes its user interface in ways that often make the software worse for my use-case.
|
||||||
|
|
||||||
|
The software could be completely perfect from my point of view, but it must and will change because of the model we're now in, there is no alternative, and if you can't innovate for real; change the UI! (see [gmail](https://), [slack](https://), [another](https://), [another](https://))
|
||||||
|
|
||||||
|
# 2. Middlemen separate creators and their fans.
|
||||||
|
Gamers are happy to pay fo excellent games. Music fans are happy to pay for excellent music. Moviegoers are happy to pay for excellent movies. In general people are happy to pay for excellent creative content of all forms.
|
||||||
|
|
||||||
|
## Prehistoric times
|
||||||
|
In the olden days, you could either listen to the radio, or go to a store and buy a record, casette or CD. You could then listen to that music as much as you liked "offline".
|
||||||
|
|
||||||
|
## Modern day
|
||||||
|
Today we can listen to pretty much any track we like from any artist instantly by using a streaming service like Spotify or Apple Music or by finding music on YouTube. This is truly like a superpower and completely mindblowing compared to how we used to listen to music!
|
||||||
|
|
||||||
|
The downside is that in order to provide this service, all the music has to be uploaded to a central service that a company owns, and the company charges a fee to stream the music to users, giving the rest to the musicians. The free that the streaming services have been charging has been increasing ever since they launched meaning that over time, the wealth is being moved from the musicians who actually create the music to the companies who stream it and now have a monopoly on those streaming services. They also gatekeep who can get onto the services.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function foo() {
|
||||||
|
return "foo"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- <div id="dbb" class="embed"> -->
|
||||||
|
<div id="dbb">
|
||||||
|
<img width="100%" src="https://dotbigbang.com/image/games/f2c27700-8e0f-453c-bcd7-4e22bc76d7ef.png?modification_date=1680421227218" onclick="document.getElementById('dbb').innerHTML='<iframe src=\'https\:\/\/dotbigbang.com/game/f2c277008e0f453cbcd74e22bc76d7ef/bobbyd-octo-original\' width=\'100%\' height=\'400px\' style=\'border:none\' allowfullscreen></iframe>' ">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <iframe src="https://dotbigbang.com/game/f2c277008e0f453cbcd74e22bc76d7ef/bobbyd-octo-original" width="640px" height="400px" style="border:none"></iframe> -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<iframe src="https://www.lexaloffle.com/bbs/widget.php?pid=dinkykong" allowfullscreen width="100%" height="513" style="border:none; overflow:hidden"></iframe>
|
||||||
|
<iframe src="https://www.lexaloffle.com/bbs/widget.php?pid=rockformetal" allowfullscreen width="100%" height="513" style="border:none; overflow:hidden"></iframe>
|
||||||
|
<!-- <img src="https://hgtvhome.sndimg.com/content/dam/images/hgtv/fullset/2018/3/22/0/shutterstock_national-puppy-day-224423782.jpg.rend.hgtvcom.966.725.suffix/1521744674350.jpeg" alt="drawing" width="200"/>
|
||||||
|
|
||||||
|
![A picture of a puppy][puppy]
|
||||||
|
|
||||||
|
[ring]: https://www.theverge.com/2019/1/10/18177305/ring-employees-unencrypted-customer-video-amazon
|
||||||
|
[puppy]: https://hgtvhome.sndimg.com/content/dam/images/hgtv/fullset/2018/3/22/0/shutterstock_national-puppy-day-224423782.jpg.rend.hgtvcom.966.725.suffix/1521744674350.jpeg -->
|
||||||
166
src/db.ts
Normal file
166
src/db.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
// interface MyJsonObject {
|
||||||
|
// id: string;
|
||||||
|
// name: string;
|
||||||
|
// email: string;
|
||||||
|
// }
|
||||||
|
|
||||||
|
const dbName: string = "ddln";
|
||||||
|
const storeNameBase: string = "posts";
|
||||||
|
let keyBase = "dandelion_posts_v1_"
|
||||||
|
let key = "";
|
||||||
|
|
||||||
|
|
||||||
|
interface IDBRequestEvent<T = any> extends Event {
|
||||||
|
target: IDBRequest<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexedDB uses DOMException, so let's use it for error typing
|
||||||
|
type DBError = Event & {
|
||||||
|
target: { errorCode: DOMException };
|
||||||
|
};
|
||||||
|
|
||||||
|
export function openDatabase(userID:string): Promise<IDBDatabase> {
|
||||||
|
const storeName = `${storeNameBase}_${userID}`;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request: IDBOpenDBRequest = indexedDB.open(dbName, 1);
|
||||||
|
|
||||||
|
request.onerror = (event: Event) => {
|
||||||
|
// Use a type assertion to access the specific properties of IDBRequest error event
|
||||||
|
const errorEvent = event as IDBRequestEvent;
|
||||||
|
reject(`Database error: ${errorEvent.target.error?.message}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
|
||||||
|
const db: IDBDatabase = (event.target as IDBOpenDBRequest).result;
|
||||||
|
if (!db.objectStoreNames.contains(storeName)) {
|
||||||
|
db.createObjectStore(storeName, { keyPath: "id", autoIncrement: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onsuccess = (event: Event) => {
|
||||||
|
const db: IDBDatabase = (event.target as IDBOpenDBRequest).result;
|
||||||
|
resolve(db);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export async function addData(userID: string, data: any): Promise<void> {
|
||||||
|
try {
|
||||||
|
const storeName = `${storeNameBase}_${userID}`;
|
||||||
|
const db = await openDatabase(userID);
|
||||||
|
const transaction = db.transaction(storeName, "readwrite");
|
||||||
|
const store = transaction.objectStore(storeName);
|
||||||
|
|
||||||
|
const addRequest = store.add(data);
|
||||||
|
|
||||||
|
addRequest.onsuccess = (e: Event) => {
|
||||||
|
// console.log('Data has been added:', (e.target as IDBRequest).result);
|
||||||
|
};
|
||||||
|
|
||||||
|
addRequest.onerror = (event: Event) => {
|
||||||
|
// Use a type assertion to access the specific properties of IDBRequest error event
|
||||||
|
const errorEvent = event as IDBRequestEvent;
|
||||||
|
console.error('Error in adding data:', errorEvent.target.error?.message);
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in opening database:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addDataArray(userID: string, array: any[]): Promise<void> {
|
||||||
|
try {
|
||||||
|
const storeName = `${storeNameBase}_${userID}`;
|
||||||
|
const db = await openDatabase(userID);
|
||||||
|
const transaction = db.transaction(storeName, "readwrite");
|
||||||
|
const store = transaction.objectStore(storeName);
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
for (let data of array) {
|
||||||
|
const addRequest = store.add(data);
|
||||||
|
addRequest.onsuccess = (e: Event) => {
|
||||||
|
// console.log('Data has been added:', (e.target as IDBRequest).result);
|
||||||
|
};
|
||||||
|
|
||||||
|
addRequest.onerror = (event: Event) => {
|
||||||
|
// Use a type assertion to access the specific properties of IDBRequest error event
|
||||||
|
const errorEvent = event as IDBRequestEvent;
|
||||||
|
console.error('Error in adding data:', errorEvent.target.error?.message);
|
||||||
|
};
|
||||||
|
|
||||||
|
count++;
|
||||||
|
|
||||||
|
if (count % 100 === 0) {
|
||||||
|
console.log(`Added ${count} posts...`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in opening database:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getData(userID:string, lowerID:number, upperID:number): Promise<any | undefined> {
|
||||||
|
const storeName = `${storeNameBase}_${userID}`;
|
||||||
|
const db = await openDatabase(userID);
|
||||||
|
const transaction = db.transaction(storeName, "readonly");
|
||||||
|
const store = transaction.objectStore(storeName);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const keyRangeValue = IDBKeyRange.bound(lowerID, upperID);
|
||||||
|
|
||||||
|
const records: any[] = [];
|
||||||
|
|
||||||
|
const cursorRequest = store.openCursor(keyRangeValue);
|
||||||
|
|
||||||
|
cursorRequest.onsuccess = (event: Event) => {
|
||||||
|
const cursor = (event.target as IDBRequest).result as IDBCursorWithValue;
|
||||||
|
if (cursor) {
|
||||||
|
records.push(cursor.value); // Collect the record
|
||||||
|
cursor.continue(); // Move to the next item in the range
|
||||||
|
} else {
|
||||||
|
// No more entries in the range
|
||||||
|
resolve(records);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
cursorRequest.onerror = (event: Event) => {
|
||||||
|
// Use a type assertion to access the specific properties of IDBRequest error event
|
||||||
|
const errorEvent = event as IDBRequestEvent;
|
||||||
|
console.error('Transaction failed:', errorEvent.target.error?.message);
|
||||||
|
reject(errorEvent.target.error); // Reject the promise if there's an error
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllData(userID:string): Promise<any | undefined> {
|
||||||
|
const storeName = `${storeNameBase}_${userID}`;
|
||||||
|
const db = await openDatabase(userID);
|
||||||
|
const transaction = db.transaction(storeName, "readonly");
|
||||||
|
const store = transaction.objectStore(storeName);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const getRequest = store.getAll();
|
||||||
|
|
||||||
|
getRequest.onsuccess = () => {
|
||||||
|
if (getRequest.result) {
|
||||||
|
// console.log('Retrieved data:', getRequest.result.jsonData);
|
||||||
|
// resolve(getRequest.result.jsonData as any);
|
||||||
|
resolve(getRequest.result);
|
||||||
|
} else {
|
||||||
|
console.log('No data record found for key', key);
|
||||||
|
resolve(undefined); // explicitly resolve with undefined when no data is found
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getRequest.onerror = (event: Event) => {
|
||||||
|
// Use a type assertion to access the specific properties of IDBRequest error event
|
||||||
|
const errorEvent = event as IDBRequestEvent;
|
||||||
|
console.error('Transaction failed:', errorEvent.target.error?.message);
|
||||||
|
reject(errorEvent.target.error); // reject the promise if there's an error
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
447
src/main.ts
Normal file
447
src/main.ts
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
import { openDatabase, getData, addData, addDataArray, getAllData } from "./db.js"
|
||||||
|
|
||||||
|
// let posts:any;
|
||||||
|
// let keyBase = "dandelion_posts_v1_"
|
||||||
|
// let key:string = "";
|
||||||
|
|
||||||
|
interface PostTimestamp {
|
||||||
|
year: number,
|
||||||
|
month: number,
|
||||||
|
day: number,
|
||||||
|
hour: number,
|
||||||
|
minute: number,
|
||||||
|
second: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
class Post {
|
||||||
|
constructor(author: string, text: string, post_timestamp: PostTimestamp, format = null) {
|
||||||
|
post_timestamp = post_timestamp;
|
||||||
|
author = author;
|
||||||
|
text = text;
|
||||||
|
format = format;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function uuidv4() {
|
||||||
|
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c: any) =>
|
||||||
|
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let logLines: string[] = [];
|
||||||
|
let logLength = 10;
|
||||||
|
function log(message: string) {
|
||||||
|
logLines.push(`${message}`);
|
||||||
|
if (logLines.length > 10) {
|
||||||
|
logLines = logLines.slice(logLines.length - logLength);
|
||||||
|
}
|
||||||
|
let log = document.getElementById("log");
|
||||||
|
if (!log) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
log.innerText = logLines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayBufferToBase64(buffer: ArrayBuffer) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const blob = new Blob([buffer], { type: 'application/octet-stream' });
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onloadend = () => {
|
||||||
|
const dataUrl = reader.result as string;
|
||||||
|
if (!dataUrl) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const base64 = dataUrl.split(',')[1];
|
||||||
|
resolve(base64);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onerror = (error) => {
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTestData() {
|
||||||
|
let postsTestData = await (await fetch("./postsTestData.json")).json();
|
||||||
|
|
||||||
|
return postsTestData;
|
||||||
|
}
|
||||||
|
|
||||||
|
let time = 0;
|
||||||
|
|
||||||
|
function timerStart() {
|
||||||
|
time = performance.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
function timerDelta() {
|
||||||
|
return performance.now() - time;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTestData2() {
|
||||||
|
let postsTestData: any[] = [];
|
||||||
|
|
||||||
|
let response = await fetch("./tweets.js");
|
||||||
|
let tweetsText = await response.text();
|
||||||
|
tweetsText = tweetsText.replace("window.YTD.tweets.part0", "window.tweetData");
|
||||||
|
|
||||||
|
new Function(tweetsText)();
|
||||||
|
|
||||||
|
|
||||||
|
// let tweets = JSON.parse(tweetJSON);
|
||||||
|
|
||||||
|
for (let entry of (window as any).tweetData) {
|
||||||
|
// if (entry.tweet.hasOwnProperty("in_reply_to_screen_name") || entry.tweet.retweeted || entry.tweet.full_text.startsWith("RT")) {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
let mediaURL: string = entry.tweet?.entities?.media?.[0]?.media_url;
|
||||||
|
let isImage = false;
|
||||||
|
if (mediaURL) {
|
||||||
|
isImage = mediaURL.includes('jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
let imageData;
|
||||||
|
let encodedImage = null;
|
||||||
|
if (isImage) {
|
||||||
|
try {
|
||||||
|
imageData = await (await fetch(mediaURL)).arrayBuffer();
|
||||||
|
encodedImage = await arrayBufferToBase64(imageData);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
postsTestData.push({
|
||||||
|
post_timestamp: {
|
||||||
|
year: 2023,
|
||||||
|
month: 10,
|
||||||
|
day: 19,
|
||||||
|
hour: 14,
|
||||||
|
minute: 53,
|
||||||
|
second: 0,
|
||||||
|
},
|
||||||
|
author: `bobbydigitales`,
|
||||||
|
text: entry.tweet.full_text,
|
||||||
|
image: encodedImage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let rant = await (await fetch('ranting.md')).text();;
|
||||||
|
|
||||||
|
postsTestData.unshift({
|
||||||
|
post_timestamp: {
|
||||||
|
year: 2023,
|
||||||
|
month: 10,
|
||||||
|
day: 19,
|
||||||
|
hour: 14,
|
||||||
|
minute: 53,
|
||||||
|
second: 0,
|
||||||
|
},
|
||||||
|
author: `bobbydigitales`,
|
||||||
|
text: rant
|
||||||
|
})
|
||||||
|
|
||||||
|
return postsTestData;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTestData3(userID: string) {
|
||||||
|
let posts = await (await (fetch('./posts.json'))).json();
|
||||||
|
|
||||||
|
return posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(key: string, posts: { author: any; text: any; image: any; }[], author: any, text: any, image: any) {
|
||||||
|
posts.push({ author: author, text: text, image: image });
|
||||||
|
localStorage.setItem(key, JSON.stringify(posts));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registerServiceWorker() {
|
||||||
|
if (!("serviceWorker" in navigator)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let registrations = await navigator.serviceWorker.getRegistrations();
|
||||||
|
if (registrations.length > 0) {
|
||||||
|
console.log("Service worker already registered.");
|
||||||
|
return registrations[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register("/sw.js")
|
||||||
|
.then((registration) => {
|
||||||
|
console.log("Service Worker registered with scope:", registration.scope);
|
||||||
|
return registration;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Service Worker registration failed:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPost(userID: string, posts: Post[], postText: string) {
|
||||||
|
let post: Post = {
|
||||||
|
author: `bobbydigitales`,
|
||||||
|
text: postText,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
posts.unshift(post);
|
||||||
|
// localStorage.setItem(key, JSON.stringify(posts));
|
||||||
|
addData(userID, post)
|
||||||
|
|
||||||
|
render(posts);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateID() {
|
||||||
|
if (self.crypto.hasOwnProperty("randomUUID")) {
|
||||||
|
return self.crypto.randomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
return uuidv4();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserID() {
|
||||||
|
let id = localStorage.getItem("dandelion_id");
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
id = generateID();
|
||||||
|
localStorage.setItem("dandelion_id", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectWebsocket(userID: string) {
|
||||||
|
let websocket = new WebSocket(`ws://${window.location.hostname}:${window.location.port}/ws`);
|
||||||
|
|
||||||
|
websocket.onopen = function (evt) {
|
||||||
|
console.log("CONNECTED");
|
||||||
|
websocket.send(`{"messageType":"connect", "id": "${userID}"}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.onclose = function (evt) {
|
||||||
|
console.log("DISCONNECTED");
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.onmessage = function (evt) {
|
||||||
|
console.log('RESPONSE: ' + evt.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.onerror = function (evt) {
|
||||||
|
console.log('ERROR: ' + evt);
|
||||||
|
};
|
||||||
|
|
||||||
|
return websocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFont(fontName: string) {
|
||||||
|
|
||||||
|
document.body.style.fontFamily = fontName;
|
||||||
|
let textArea = document.getElementById('textarea_post');
|
||||||
|
if (!textArea) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
textArea.style.fontFamily = fontName;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initOffline() {
|
||||||
|
// Event listener for going offline
|
||||||
|
window.addEventListener('offline', () => { log("offline") });
|
||||||
|
|
||||||
|
// Event listener for going online
|
||||||
|
window.addEventListener('online', () => { log("online") });
|
||||||
|
|
||||||
|
log(`${navigator.onLine ? "online" : "offline"}`)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function initButtons(userID: string, posts: Post[]) {
|
||||||
|
let font1Button = document.getElementById("button_font1") as HTMLButtonElement;
|
||||||
|
let font2Button = document.getElementById("button_font2") as HTMLButtonElement;
|
||||||
|
|
||||||
|
font1Button.addEventListener('click', () => { setFont('Bookerly'); });
|
||||||
|
font2Button.addEventListener('click', () => { setFont('Virgil') });
|
||||||
|
|
||||||
|
|
||||||
|
let postButton = document.getElementById("button_post") as HTMLButtonElement;
|
||||||
|
let postText = document.getElementById("textarea_post") as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
if (!(postButton && postText)) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
postButton.addEventListener("click", () => {
|
||||||
|
addPost(userID, posts, postText.value);
|
||||||
|
postText.value = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPosts(userID: string) {
|
||||||
|
|
||||||
|
timerStart();
|
||||||
|
let posts = await getData(userID, 1, 500);
|
||||||
|
|
||||||
|
if (posts.length > 0) {
|
||||||
|
log(`Loaded ${posts.length} posts in ${timerDelta().toFixed(2)}ms`);
|
||||||
|
return posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
posts = await createTestData3(userID);
|
||||||
|
|
||||||
|
log("Adding test data...");
|
||||||
|
addDataArray(userID, posts);
|
||||||
|
|
||||||
|
// let count = 0;
|
||||||
|
// for (let post of posts) {
|
||||||
|
// debugger;
|
||||||
|
// await addData(userID, post);
|
||||||
|
// count++;
|
||||||
|
// if (count % 100 === 0) {
|
||||||
|
// log(`Added ${count} posts...`);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// log("Finished!");
|
||||||
|
|
||||||
|
|
||||||
|
return await getData(userID, 1, 100);
|
||||||
|
|
||||||
|
|
||||||
|
// debugger;
|
||||||
|
|
||||||
|
// let postsJSON = await getData(userID)
|
||||||
|
|
||||||
|
// if (!postsJSON) {
|
||||||
|
// let testPosts = await createTestData3(userID);
|
||||||
|
// for (let post of testPosts) {
|
||||||
|
// await addData(userID, post);
|
||||||
|
// }
|
||||||
|
// // localStorage.setItem(key, postsJSON);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let delta = timerDelta();
|
||||||
|
// if (postsJSON) {
|
||||||
|
// log(`read ${(postsJSON.length / 1024 / 1024).toFixed(2)}Mb from indexedDB in ${delta.toFixed(2)}ms`);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let posts = [];
|
||||||
|
// try {
|
||||||
|
// timerStart();
|
||||||
|
// posts = JSON.parse(postsJSON);
|
||||||
|
// delta = timerDelta();
|
||||||
|
// log(`parsed ${posts.length} posts from indexedDB in ${delta.toFixed(2)}ms`);
|
||||||
|
// } catch (e) {
|
||||||
|
// log("Couldn't read posts from local storage, resetting...");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!posts) {
|
||||||
|
// await createTestData3();
|
||||||
|
// // localStorage.setItem(key, JSON.stringify(testData));
|
||||||
|
// posts = testData;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let posts: Post[] = [];
|
||||||
|
let time = 0; ``
|
||||||
|
let delta = 0;
|
||||||
|
|
||||||
|
let urlParams = (new URL(window.location.href)).searchParams;
|
||||||
|
|
||||||
|
if (urlParams.get("sw") === "true") {
|
||||||
|
let registration = await registerServiceWorker();
|
||||||
|
}
|
||||||
|
|
||||||
|
let userID = getUserID();
|
||||||
|
|
||||||
|
|
||||||
|
log(`Your user ID is: ${userID}`);
|
||||||
|
|
||||||
|
if (navigator.storage && navigator.storage.persist && !navigator.storage.persisted) {
|
||||||
|
const isPersisted = await navigator.storage.persist();
|
||||||
|
console.log(`Persisted storage granted: ${isPersisted}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let websocket = connectWebsocket(userID);
|
||||||
|
initOffline();
|
||||||
|
initButtons(userID, posts);
|
||||||
|
|
||||||
|
// let main = await fetch("/main.js");
|
||||||
|
// let code = await main.text();
|
||||||
|
// console.log(code);
|
||||||
|
// registration.active.postMessage({type:"updateMain", code:code});
|
||||||
|
|
||||||
|
posts = await loadPosts(userID);
|
||||||
|
// debugger;
|
||||||
|
|
||||||
|
timerStart();
|
||||||
|
render(posts);
|
||||||
|
let renderTime = timerDelta();
|
||||||
|
|
||||||
|
log(`render took: ${renderTime.toFixed(2)}ms`);
|
||||||
|
|
||||||
|
log(`memory used: ${((performance as any).memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}Mb`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(posts: Post[]) {
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
let contentDiv = document.getElementById("content");
|
||||||
|
if (!contentDiv) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
contentDiv.innerHTML = "";
|
||||||
|
let count = 0;
|
||||||
|
for (const postData of posts) {
|
||||||
|
let post = renderPost(postData);
|
||||||
|
|
||||||
|
fragment.appendChild(post);
|
||||||
|
|
||||||
|
count++;
|
||||||
|
if (count > 500) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!contentDiv) {
|
||||||
|
throw new Error("Couldn't get content div!");
|
||||||
|
}
|
||||||
|
|
||||||
|
contentDiv.appendChild(fragment);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPost(post: Post) {
|
||||||
|
if (!(post.hasOwnProperty("text"))) {
|
||||||
|
throw new Error("Post is malformed!");
|
||||||
|
}
|
||||||
|
let containerDiv = document.createElement("div");
|
||||||
|
let textDiv = document.createElement("div");
|
||||||
|
let hr = document.createElement("hr");
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
textDiv.innerHTML = marked.parse(post.text);
|
||||||
|
containerDiv.appendChild(hr);
|
||||||
|
containerDiv.appendChild(textDiv);
|
||||||
|
|
||||||
|
if (!("image" in post && post.image)) {
|
||||||
|
return containerDiv;
|
||||||
|
}
|
||||||
|
|
||||||
|
let image = document.createElement("img");
|
||||||
|
image.src = image.src = "data:image/png;base64," + post.image;
|
||||||
|
|
||||||
|
containerDiv.appendChild(image);
|
||||||
|
return containerDiv;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("load", main);
|
||||||
27
src/postsTestData.json
Normal file
27
src/postsTestData.json
Normal file
File diff suppressed because one or more lines are too long
72
src/sw.ts
Normal file
72
src/sw.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// Establish a cache name
|
||||||
|
const cacheName = "dandelion_cache_v1";
|
||||||
|
|
||||||
|
const contentToCache = [
|
||||||
|
"/index.html",
|
||||||
|
"/main.js",
|
||||||
|
"/marked.min.js",
|
||||||
|
"/db.js",
|
||||||
|
"/bookerly.woff2",
|
||||||
|
"/virgil.woff2",
|
||||||
|
"/favicon.ico"
|
||||||
|
];
|
||||||
|
|
||||||
|
self.addEventListener("install", (e:any) => {
|
||||||
|
e.waitUntil(
|
||||||
|
(async () => {
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
console.log(
|
||||||
|
"[Service Worker] Caching all: app shell and content",
|
||||||
|
contentToCache
|
||||||
|
);
|
||||||
|
await cache.addAll(contentToCache);
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("fetch", (e:any) => {
|
||||||
|
e.respondWith(
|
||||||
|
(async () => {
|
||||||
|
const r = await caches.match(e.request);
|
||||||
|
if (r) {
|
||||||
|
console.log(
|
||||||
|
`[Service Worker] Cache hit for resource: ${e.request.url}`
|
||||||
|
);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
console.log(
|
||||||
|
`[Service Worker] Cache miss, attempting to fetch resource: ${e.request.url}`
|
||||||
|
);
|
||||||
|
|
||||||
|
response = await fetch(e.request);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
console.log(
|
||||||
|
`[Service Worker] Adding resource to cache: ${e.request.url}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
throw new Error(`Failed to fetch resource: ${e.request.url}`)
|
||||||
|
}
|
||||||
|
cache.put(e.request, response.clone());
|
||||||
|
return response;
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
addEventListener("message", async (e) => {
|
||||||
|
console.log(`Message received: ${e.data}`);
|
||||||
|
|
||||||
|
switch (e.data.type) {
|
||||||
|
case "updateMain":
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
console.log(`[Service Worker] Caching new resource: main.js`);
|
||||||
|
cache.put("/main.js", new Response());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
52
sw.js
52
sw.js
@@ -1,52 +0,0 @@
|
|||||||
// Establish a cache name
|
|
||||||
const cacheName = 'dandelion_cache_v1';
|
|
||||||
|
|
||||||
const contentToCache = [
|
|
||||||
"/index.html",
|
|
||||||
"/main.js",
|
|
||||||
"/favicon.ico",
|
|
||||||
];
|
|
||||||
|
|
||||||
self.addEventListener('install', (e) => {
|
|
||||||
e.waitUntil(
|
|
||||||
(async () => {
|
|
||||||
const cache = await caches.open(cacheName);
|
|
||||||
console.log("[Service Worker] Caching all: app shell and content", contentToCache);
|
|
||||||
await cache.addAll(contentToCache);
|
|
||||||
})(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener("fetch", (e) => {
|
|
||||||
e.respondWith(
|
|
||||||
(async () => {
|
|
||||||
const r = await caches.match(e.request);
|
|
||||||
console.log(`[Service Worker] Fetching resource: ${e.request.url}`);
|
|
||||||
if (r) {
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const response = await fetch(e.request);
|
|
||||||
} catch(e) {
|
|
||||||
console.warn(e);
|
|
||||||
}
|
|
||||||
const cache = await caches.open(cacheName);
|
|
||||||
console.log(`[Service Worker] Caching new resource: ${e.request.url}`);
|
|
||||||
cache.put(e.request, response.clone());
|
|
||||||
return response;
|
|
||||||
})(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
addEventListener("message", async (e) => {
|
|
||||||
console.log(`Message received: ${e.data}`);
|
|
||||||
|
|
||||||
switch (e.data.type) {
|
|
||||||
case "updateMain":
|
|
||||||
const cache = await caches.open(cacheName);
|
|
||||||
console.log(`[Service Worker] Caching new resource: main.js`);
|
|
||||||
cache.put("/main.js", new Response());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
79
tsconfig.json
Normal file
79
tsconfig.json
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||||
|
|
||||||
|
/* Basic Options */
|
||||||
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
|
"target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
|
||||||
|
"module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||||
|
"lib": ["ES2020", "DOM", "webworker"], /* Specify library files to be included in the compilation. */
|
||||||
|
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
|
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
|
||||||
|
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
|
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
|
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||||
|
"outDir": "./", /* Redirect output structure to the directory. */
|
||||||
|
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
|
// "composite": true, /* Enable project compilation */
|
||||||
|
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||||
|
// "removeComments": true, /* Do not emit comments to output. */
|
||||||
|
// "noEmit": true, /* Do not emit outputs. */
|
||||||
|
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||||
|
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||||
|
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||||
|
|
||||||
|
/* 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. */
|
||||||
|
|
||||||
|
/* Additional Checks */
|
||||||
|
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||||
|
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
|
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
|
||||||
|
|
||||||
|
/* Module Resolution Options */
|
||||||
|
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
|
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||||
|
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
|
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||||
|
// "types": [], /* Type declaration files to be included in compilation. */
|
||||||
|
// "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'. */
|
||||||
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
|
||||||
|
/* Source Map Options */
|
||||||
|
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
"inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||||
|
"inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||||
|
|
||||||
|
/* Experimental Options */
|
||||||
|
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
|
|
||||||
|
/* Advanced Options */
|
||||||
|
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||||
|
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./src/**/*.ts" // Include all TypeScript files in the src directory and its subdirectories
|
||||||
|
],
|
||||||
|
|
||||||
|
"exclude": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
virgil.woff2
Normal file
BIN
virgil.woff2
Normal file
Binary file not shown.
Reference in New Issue
Block a user