1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-06-27 16:36:00 +00:00

PWA: First implementation of offline mode

This commit is contained in:
Brieuc Dubois 2024-06-21 19:13:54 +02:00
parent 05f7a34d43
commit 925ea2c082
10 changed files with 1851 additions and 1076 deletions

View file

@ -42,6 +42,7 @@ type User struct {
DefaultHomePage string `json:"default_home_page"` DefaultHomePage string `json:"default_home_page"`
CategoriesSortingOrder string `json:"categories_sorting_order"` CategoriesSortingOrder string `json:"categories_sorting_order"`
MarkReadOnView bool `json:"mark_read_on_view"` MarkReadOnView bool `json:"mark_read_on_view"`
CacheForOffline bool `json:"cache_for_offline"`
MediaPlaybackRate float64 `json:"media_playback_rate"` MediaPlaybackRate float64 `json:"media_playback_rate"`
BlockFilterEntryRules string `json:"block_filter_entry_rules"` BlockFilterEntryRules string `json:"block_filter_entry_rules"`
KeepFilterEntryRules string `json:"keep_filter_entry_rules"` KeepFilterEntryRules string `json:"keep_filter_entry_rules"`

View file

@ -970,6 +970,7 @@ var migrations = []func(tx *sql.Tx, driver string) error{
return err return err
}, },
func(tx *sql.Tx, _ string) (err error) { func(tx *sql.Tx, _ string) (err error) {
<<<<<<< HEAD
sql := ` sql := `
ALTER TABLE integrations ADD COLUMN discord_enabled bool default 'f'; ALTER TABLE integrations ADD COLUMN discord_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN discord_webhook_link text default ''; ALTER TABLE integrations ADD COLUMN discord_webhook_link text default '';
@ -1015,4 +1016,9 @@ var migrations = []func(tx *sql.Tx, driver string) error{
_, err = tx.Exec(sql) _, err = tx.Exec(sql)
return err return err
}, },
func(tx *sql.Tx, _ string) (err error) {
sql := `ALTER TABLE users ADD COLUMN cache_for_offline boolean default 'f'`
_, err = tx.Exec(sql)
return err
},
} }

File diff suppressed because it is too large Load diff

View file

@ -41,6 +41,7 @@ type User struct {
MediaPlaybackRate float64 `json:"media_playback_rate"` MediaPlaybackRate float64 `json:"media_playback_rate"`
BlockFilterEntryRules string `json:"block_filter_entry_rules"` BlockFilterEntryRules string `json:"block_filter_entry_rules"`
KeepFilterEntryRules string `json:"keep_filter_entry_rules"` KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
CacheForOffline bool `json:"cache_for_offline"`
} }
// UserCreationRequest represents the request to create a user. // UserCreationRequest represents the request to create a user.
@ -82,6 +83,7 @@ type UserModificationRequest struct {
MediaPlaybackRate *float64 `json:"media_playback_rate"` MediaPlaybackRate *float64 `json:"media_playback_rate"`
BlockFilterEntryRules *string `json:"block_filter_entry_rules"` BlockFilterEntryRules *string `json:"block_filter_entry_rules"`
KeepFilterEntryRules *string `json:"keep_filter_entry_rules"` KeepFilterEntryRules *string `json:"keep_filter_entry_rules"`
CacheForOffline *bool `json:"cache_for_offline"`
} }
// Patch updates the User object with the modification request. // Patch updates the User object with the modification request.
@ -197,6 +199,9 @@ func (u *UserModificationRequest) Patch(user *User) {
if u.KeepFilterEntryRules != nil { if u.KeepFilterEntryRules != nil {
user.KeepFilterEntryRules = *u.KeepFilterEntryRules user.KeepFilterEntryRules = *u.KeepFilterEntryRules
} }
if u.CacheForOffline != nil {
user.CacheForOffline = *u.CacheForOffline
}
} }
// UseTimezone converts last login date to the given timezone. // UseTimezone converts last login date to the given timezone.

View file

@ -96,7 +96,8 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
mark_read_on_view, mark_read_on_view,
media_playback_rate, media_playback_rate,
block_filter_entry_rules, block_filter_entry_rules,
keep_filter_entry_rules keep_filter_entry_rules,
cache_for_offline
` `
tx, err := s.db.Begin() tx, err := s.db.Begin()
@ -140,6 +141,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
&user.MediaPlaybackRate, &user.MediaPlaybackRate,
&user.BlockFilterEntryRules, &user.BlockFilterEntryRules,
&user.KeepFilterEntryRules, &user.KeepFilterEntryRules,
&user.CacheForOffline,
) )
if err != nil { if err != nil {
tx.Rollback() tx.Rollback()
@ -204,9 +206,10 @@ func (s *Storage) UpdateUser(user *model.User) error {
mark_read_on_media_player_completion=$25, mark_read_on_media_player_completion=$25,
media_playback_rate=$26, media_playback_rate=$26,
block_filter_entry_rules=$27, block_filter_entry_rules=$27,
keep_filter_entry_rules=$28 keep_filter_entry_rules=$28,
cache_for_offline=$29
WHERE WHERE
id=$29 id=$30
` `
_, err = s.db.Exec( _, err = s.db.Exec(
@ -239,6 +242,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
user.MediaPlaybackRate, user.MediaPlaybackRate,
user.BlockFilterEntryRules, user.BlockFilterEntryRules,
user.KeepFilterEntryRules, user.KeepFilterEntryRules,
user.CacheForOffline,
user.ID, user.ID,
) )
if err != nil { if err != nil {
@ -273,9 +277,10 @@ func (s *Storage) UpdateUser(user *model.User) error {
mark_read_on_media_player_completion=$24, mark_read_on_media_player_completion=$24,
media_playback_rate=$25, media_playback_rate=$25,
block_filter_entry_rules=$26, block_filter_entry_rules=$26,
keep_filter_entry_rules=$27 keep_filter_entry_rules=$27,
cache_for_offline=$28
WHERE WHERE
id=$28 id=$29
` `
_, err := s.db.Exec( _, err := s.db.Exec(
@ -307,6 +312,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
user.MediaPlaybackRate, user.MediaPlaybackRate,
user.BlockFilterEntryRules, user.BlockFilterEntryRules,
user.KeepFilterEntryRules, user.KeepFilterEntryRules,
user.CacheForOffline,
user.ID, user.ID,
) )
@ -360,7 +366,8 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) {
mark_read_on_media_player_completion, mark_read_on_media_player_completion,
media_playback_rate, media_playback_rate,
block_filter_entry_rules, block_filter_entry_rules,
keep_filter_entry_rules keep_filter_entry_rules,
cache_for_offline
FROM FROM
users users
WHERE WHERE
@ -401,7 +408,8 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) {
mark_read_on_media_player_completion, mark_read_on_media_player_completion,
media_playback_rate, media_playback_rate,
block_filter_entry_rules, block_filter_entry_rules,
keep_filter_entry_rules keep_filter_entry_rules,
cache_for_offline
FROM FROM
users users
WHERE WHERE
@ -442,7 +450,8 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) {
mark_read_on_media_player_completion, mark_read_on_media_player_completion,
media_playback_rate, media_playback_rate,
block_filter_entry_rules, block_filter_entry_rules,
keep_filter_entry_rules keep_filter_entry_rules,
cache_for_offline
FROM FROM
users users
WHERE WHERE
@ -490,7 +499,8 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) {
u.mark_read_on_media_player_completion, u.mark_read_on_media_player_completion,
media_playback_rate, media_playback_rate,
u.block_filter_entry_rules, u.block_filter_entry_rules,
u.keep_filter_entry_rules u.keep_filter_entry_rules,
u.cache_for_offline
FROM FROM
users u users u
LEFT JOIN LEFT JOIN
@ -533,6 +543,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err
&user.MediaPlaybackRate, &user.MediaPlaybackRate,
&user.BlockFilterEntryRules, &user.BlockFilterEntryRules,
&user.KeepFilterEntryRules, &user.KeepFilterEntryRules,
&user.CacheForOffline,
) )
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@ -646,7 +657,9 @@ func (s *Storage) Users() (model.Users, error) {
mark_read_on_media_player_completion, mark_read_on_media_player_completion,
media_playback_rate, media_playback_rate,
block_filter_entry_rules, block_filter_entry_rules,
keep_filter_entry_rules keep_filter_entry_rules,
media_playback_rate,
cache_for_offline
FROM FROM
users users
ORDER BY username ASC ORDER BY username ASC
@ -690,6 +703,7 @@ func (s *Storage) Users() (model.Users, error) {
&user.MediaPlaybackRate, &user.MediaPlaybackRate,
&user.BlockFilterEntryRules, &user.BlockFilterEntryRules,
&user.KeepFilterEntryRules, &user.KeepFilterEntryRules,
&user.CacheForOffline,
) )
if err != nil { if err != nil {

View file

@ -52,6 +52,7 @@ type SettingsForm struct {
MediaPlaybackRate float64 MediaPlaybackRate float64
BlockFilterEntryRules string BlockFilterEntryRules string
KeepFilterEntryRules string KeepFilterEntryRules string
CacheForOffline bool
} }
// MarkAsReadBehavior returns the MarkReadBehavior from the given MarkReadOnView and MarkReadOnMediaPlayerCompletion values. // MarkAsReadBehavior returns the MarkReadBehavior from the given MarkReadOnView and MarkReadOnMediaPlayerCompletion values.
@ -119,6 +120,8 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
user.MarkReadOnView = MarkReadOnView user.MarkReadOnView = MarkReadOnView
user.MarkReadOnMediaPlayerCompletion = MarkReadOnMediaPlayerCompletion user.MarkReadOnMediaPlayerCompletion = MarkReadOnMediaPlayerCompletion
user.CacheForOffline = s.CacheForOffline
if s.Password != "" { if s.Password != "" {
user.Password = s.Password user.Password = s.Password
} }
@ -205,5 +208,6 @@ func NewSettingsForm(r *http.Request) *SettingsForm {
MediaPlaybackRate: mediaPlaybackRate, MediaPlaybackRate: mediaPlaybackRate,
BlockFilterEntryRules: r.FormValue("block_filter_entry_rules"), BlockFilterEntryRules: r.FormValue("block_filter_entry_rules"),
KeepFilterEntryRules: r.FormValue("keep_filter_entry_rules"), KeepFilterEntryRules: r.FormValue("keep_filter_entry_rules"),
CacheForOffline: r.FormValue("cache_for_offline") == "1",
} }
} }

View file

@ -46,6 +46,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
MediaPlaybackRate: user.MediaPlaybackRate, MediaPlaybackRate: user.MediaPlaybackRate,
BlockFilterEntryRules: user.BlockFilterEntryRules, BlockFilterEntryRules: user.BlockFilterEntryRules,
KeepFilterEntryRules: user.KeepFilterEntryRules, KeepFilterEntryRules: user.KeepFilterEntryRules,
CacheForOffline: user.CacheForOffline,
} }
timezones, err := h.store.Timezones() timezones, err := h.store.Timezones()

File diff suppressed because it is too large Load diff

View file

@ -1,44 +1,66 @@
// Incrementing OFFLINE_VERSION will kick off the install event and force // Incrementing OFFLINE_VERSION will kick off the install event and force
// previously cached resources to be updated from the network. // previously cached resources to be updated from the network.
const OFFLINE_VERSION = 1; const OFFLINE_VERSION = 2;
const CACHE_NAME = "offline"; const CACHE_NAME = "offline";
console.log(USE_CACHE);
self.addEventListener("install", (event) => { self.addEventListener("install", (event) => {
event.waitUntil( event.waitUntil(
(async () => { (async () => {
const cache = await caches.open(CACHE_NAME); const cache = await caches.open(CACHE_NAME);
// Setting {cache: 'reload'} in the new request will ensure that the if (USE_CACHE) {
// response isn't fulfilled from the HTTP cache; i.e., it will be from await cache.addAll(["/", "/unread", OFFLINE_URL]);
// the network. } else {
await cache.add(new Request(OFFLINE_URL, { cache: "reload" })); // Setting {cache: 'reload'} in the new request will ensure that the
})() // response isn't fulfilled from the HTTP cache; i.e., it will be from
); // the network.
await cache.add(new Request(OFFLINE_URL, { cache: "reload" }));
}
})(),
);
// Force the waiting service worker to become the active service worker. // Force the waiting service worker to become the active service worker.
self.skipWaiting(); self.skipWaiting();
}); });
self.addEventListener("fetch", (event) => { self.addEventListener("fetch", (event) => {
// We proxify requests through fetch() only if we are offline because it's slower. // We proxify requests through fetch() only if we are offline because it's slower.
if (navigator.onLine === false && event.request.mode === "navigate") { if (
event.respondWith( USE_CACHE ||
(async () => { (navigator.onLine === false && event.request.mode === "navigate")
try { ) {
// Always try the network first. event.respondWith(
const networkResponse = await fetch(event.request); (async () => {
return networkResponse; try {
} catch (error) { // Always try the network first.
// catch is only triggered if an exception is thrown, which is likely const networkResponse = await fetch(event.request);
// due to a network error. if (USE_CACHE) {
// If fetch() returns a valid HTTP response with a response code in const cache = await caches.open(CACHE_NAME);
// the 4xx or 5xx range, the catch() will NOT be called. cache.put(event.request, networkResponse.clone());
const cache = await caches.open(CACHE_NAME); }
const cachedResponse = await cache.match(OFFLINE_URL); return networkResponse;
return cachedResponse; } catch (error) {
} // catch is only triggered if an exception is thrown, which is likely
})() // due to a network error.
); // If fetch() returns a valid HTTP response with a response code in
} // the 4xx or 5xx range, the catch() will NOT be called.
const cache = await caches.open(CACHE_NAME);
if (!USE_CACHE) {
return await cache.match(OFFLINE_URL);
}
const cachedResponse = await cache.match(event.request);
if (cachedResponse) {
return cachedResponse;
}
return await cache.match(OFFLINE_URL);
}
})(),
);
}
}); });

View file

@ -31,7 +31,19 @@ func (h *handler) showJavascript(w http.ResponseWriter, r *http.Request) {
contents := static.JavascriptBundles[filename] contents := static.JavascriptBundles[filename]
if filename == "service-worker" { if filename == "service-worker" {
variables := fmt.Sprintf(`const OFFLINE_URL=%q;`, route.Path(h.router, "offline")) user, err := h.store.UserByID(request.UserID(r))
if err != nil {
html.ServerError(w, r, err)
return
}
cacheForOffline := 0
if user.CacheForOffline {
cacheForOffline = 1
}
variables := fmt.Sprintf(`const OFFLINE_URL=%q;const USE_CACHE=%d;`, route.Path(h.router, "offline"), cacheForOffline)
contents = append([]byte(variables), contents...) contents = append([]byte(variables), contents...)
} }