Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ onEnvironment(({ logStoreCreator }) => {
export function getClient(): CrossTabClient {
let logux = client.get()
if (!logux) {
/* c8 ignore next 2 */
throw new Error('No Slow Reader client')
}
return logux
Expand Down
75 changes: 75 additions & 0 deletions core/current-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { computed, type ReadableAtom, type WritableStore } from 'nanostores'

import { getEnvironment } from './environment.ts'
import { type Page, pages } from './pages/index.ts'
import { type Route, router } from './router.ts'

function isStore(store: unknown): store is WritableStore {
return typeof store === 'object' && store !== null && 'listen' in store
}

function eachParam<SomeRoute extends Route>(
page: Page<SomeRoute['route']>,
route: SomeRoute,
iterator: <Param extends keyof SomeRoute['params']>(
store: WritableStore<SomeRoute['params'][Param]>,
name: Param,
value: SomeRoute['params'][Param]
) => void
): void {
let params = route.params as SomeRoute['params']
for (let i in params) {
let name = i as keyof SomeRoute['params']
let value = params[name]
let store = page[name]
if (isStore(store)) {
iterator(store, name, value)
}
}
}

function changeRouteParam(
route: Route,
change: Partial<Route['params']>
): void {
getEnvironment().openRoute({
...route,
params: {
...route.params,
...change
}
} as Route)
}

let prevPage: Page | undefined
let unbinds: (() => void)[] = []

export const currentPage: ReadableAtom<Page> = computed(router, route => {
let page = pages[route.route]() as Page
if (page !== prevPage) {
if (prevPage) {
for (let unbind of unbinds) unbind()
prevPage.destroy()
}
prevPage = page

eachParam(page, route, (store, param) => {
unbinds.push(
store.listen(newValue => {
let currentRoute = router.get()
if (currentRoute.route === page.route) {
changeRouteParam(currentRoute, { [param]: newValue })
}
})
)
})
}

eachParam(page, route, (store, param, value) => {
if (store.get() !== value) {
store.set(value)
}
})

return page
})
26 changes: 21 additions & 5 deletions core/download.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { warning } from './devtools.ts'
import { request } from './request.ts'

let cache = new Map<string, Response>()

export interface TextResponse {
readonly contentType: string
readonly headers: Headers
Expand All @@ -14,7 +16,7 @@ export interface TextResponse {
}

export interface DownloadTask {
abortAll(): void
destroy(): void
request: typeof request
text(...args: Parameters<typeof request>): Promise<TextResponse>
}
Expand Down Expand Up @@ -107,18 +109,32 @@ export function createTextResponse(
}
}

export function createDownloadTask(): DownloadTask {
export function createDownloadTask(
taskOpts: { cache?: 'read' | 'write' | false } = {}
): DownloadTask {
let controller = new AbortController()
let cached: string[] = []
return {
abortAll() {
destroy() {
for (let url of cached) cache.delete(url)
controller.abort()
},
request(url, opts = {}) {
return request(url, {
async request(url, opts = {}) {
if (taskOpts.cache) {
let fromCache = cache.get(url)
if (fromCache) return fromCache.clone()
}

let response = await request(url, {
redirect: 'follow',
signal: controller.signal,
...opts
})
if (taskOpts.cache === 'write') {
cached.push(url)
cache.set(url, response.clone())
}
return response
},
async text(url, opts) {
let response = await this.request(url, opts)
Expand Down
19 changes: 16 additions & 3 deletions core/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ function runEnvListener(listener: EnvironmentListener): void {

export function onEnvironment(cb: EnvironmentListener): void {
if (currentEnvironment) {
/* c8 ignore next 2 */
runEnvListener(cb)
}
listeners.push(cb)
Expand Down Expand Up @@ -113,6 +114,7 @@ export function setupEnvironment<Router extends BaseRouter>(

export function getEnvironment(): Environment {
if (!currentEnvironment) {
/* c8 ignore next 2 */
throw new Error('No Slow Reader environment')
}
return currentEnvironment
Expand All @@ -126,8 +128,17 @@ export function setIsMobile(isSmallScreen: boolean): void {

const testRouter = atom<BaseRoute | undefined>()

export function setBaseTestRoute(route: BaseRoute | undefined): void {
testRouter.set(route)
export function addHashToBaseRoute(
route: BaseRoute | Omit<BaseRoute, 'hash'> | undefined
): BaseRoute | undefined {
if (!route) return undefined
return { hash: '', ...route } as BaseRoute
}

export function setBaseTestRoute(
route: BaseRoute | Omit<BaseRoute, 'hash'> | undefined
): void {
testRouter.set(addHashToBaseRoute(route))
}

export function getTestEnvironment(): EnvironmentAndStore {
Expand All @@ -137,7 +148,9 @@ export function getTestEnvironment(): EnvironmentAndStore {
locale: atom('en'),
logStoreCreator: () => new MemoryStore(),
networkType: () => ({ saveData: undefined, type: undefined }),
openRoute: setBaseTestRoute,
openRoute: route => {
setBaseTestRoute({ ...route, hash: '' })
},
persistentEvents: { addEventListener() {}, removeEventListener() {} },
persistentStore: {},
restartApp: () => {},
Expand Down
1 change: 1 addition & 0 deletions core/fast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ onEnvironment(({ openRoute }) => {
if (notSynced(router.get())) {
openRoute({
params: { category, since },
popups: [],
route: 'fast'
})
}
Expand Down
3 changes: 3 additions & 0 deletions core/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ let $locale = atom('en')

let loader: TranslationLoader

/* c8 ignore start */
// TODO: Until we will have real translations
export const i18n = createI18n($locale, {
get(...args) {
return loader(...args)
Expand All @@ -19,3 +21,4 @@ onEnvironment(({ locale, translationLoader }) => {
$locale.set(value)
})
})
/* c8 ignore stop */
5 changes: 5 additions & 0 deletions core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './busy.ts'
export * from './category.ts'
export * from './client.ts'
export * from './comfort.ts'
export * from './current-page.ts'
export * from './devtools.ts'
export * from './download.ts'
export * from './environment.ts'
Expand All @@ -13,10 +14,14 @@ export * from './filter.ts'
export * from './html.ts'
export * from './i18n.ts'
export * from './import.ts'
export { waitLoading } from './lib/stores.ts'
export * from './loader/index.ts'
export * from './menu.ts'
export * from './messages/index.ts'
export * from './not-found.ts'
export * from './opened-popups.ts'
export * from './pages/index.ts'
export * from './popups/index.ts'
export * from './post.ts'
export * from './posts-list.ts'
export * from './preview.ts'
Expand Down
30 changes: 30 additions & 0 deletions core/lib/stores.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { SyncMapValues } from '@logux/actions'
import type { LoadedSyncMap, SyncMapStore } from '@logux/client'
import type {
MapStore,
ReadableAtom,
Expand Down Expand Up @@ -67,3 +69,31 @@ export function computeFrom<Value, SourceStores extends ReadableAtom[]>(
}
})
}

export function waitLoading<Value extends boolean>(
store: ReadableAtom<Value>
): Promise<void> {
return new Promise<void>(resolve => {
let unbind = store.listen(state => {
if (state === false) {
unbind()
resolve()
}
})
})
}

export async function waitSyncLoading<Value extends SyncMapValues>(
store: SyncMapStore<Value>
): Promise<LoadedSyncMap<SyncMapStore<Value>>> {
let value = store.get()
if (value.isLoading) {
let unbind = store.listen(() => {})
try {
await store.loading
} finally {
unbind()
}
}
return store as LoadedSyncMap<SyncMapStore<Value>>
}
30 changes: 30 additions & 0 deletions core/loader/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,36 @@ export const loaders = {
atom,
jsonFeed,
rss
} satisfies {
[Name in string]: Loader
}

export type LoaderName = keyof typeof loaders

export interface FeedLoader {
loader: Loader
name: LoaderName
title: string
url: string
}

export function getLoaderForText(response: TextResponse): false | FeedLoader {
if (response.ok) {
let names = Object.keys(loaders) as LoaderName[]
let parsed = new URL(response.url)
for (let name of names) {
if (loaders[name].isMineUrl(parsed) !== false) {
let title = loaders[name].isMineText(response)
if (title !== false) {
return {
loader: loaders[name],
name,
title: title.trim(),
url: response.url
}
}
}
}
}
return false
}
27 changes: 22 additions & 5 deletions core/not-found.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
import { LoguxUndoError } from '@logux/client'
import type { LoguxUndoError } from '@logux/client'
import { atom } from 'nanostores'

import { onEnvironment } from './environment.ts'
import { router } from './router.ts'

export const notFound = atom(false)

export class NotFoundError extends Error {
constructor() {
super('Not found')
this.name = 'NotFoundError'
Error.captureStackTrace(this, NotFoundError)
}
}

export function isNotFoundError(
error: unknown
): error is LoguxUndoError | NotFoundError {
if (error instanceof Error) {
return (
error.name === 'NotFoundError' ||
(error.name === 'LoguxUndoError' && error.message.includes('notFound'))
)
}
return false
}

onEnvironment(({ errorEvents }) => {
errorEvents.addEventListener('unhandledrejection', event => {
if (
event.reason instanceof LoguxUndoError &&
event.reason.message.includes('notFound')
) {
if (isNotFoundError(event.reason)) {
notFound.set(true)
}

Expand Down
29 changes: 29 additions & 0 deletions core/opened-popups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { computed, type ReadableAtom } from 'nanostores'

import { type Popup, popups } from './popups/index.ts'
import { router } from './router.ts'

let prevPopups: Popup[] = []

export const openedPopups: ReadableAtom<Popup[]> = computed(router, route => {
let lastIndex = 0
let nextPopups = route.popups.map((popup, index) => {
lastIndex = index
let prevPopup = prevPopups[index]
if (
prevPopup &&
prevPopup.name === popup.popup &&
prevPopup.param === popup.param
) {
return prevPopup
} else {
if (prevPopup) prevPopup.destroy()
return popups[popup.popup](popup.param)
}
})
for (let closedPopup of prevPopups.slice(lastIndex + 1)) {
closedPopup.destroy()
}
prevPopups = nextPopups
return nextPopups
})
2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"./package.json": "./package.json"
},
"dependencies": {
"@logux/client": "0.21.1",
"@logux/client": "github:logux/client#next",
"@logux/core": "0.9.0",
"@nanostores/i18n": "0.12.2",
"@nanostores/persistent": "0.10.2",
Expand Down
Loading