Skip to content

Commit 11969b3

Browse files
authored
Caching (Store board state locally in localStorage) (#667)
1 parent 62ccf1b commit 11969b3

File tree

9 files changed

+162
-13
lines changed

9 files changed

+162
-13
lines changed

src-ui/css/ui.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,19 @@ form#fileform2 {
614614
content: "+";
615615
}
616616

617+
div#saveicon {
618+
float: right;
619+
position: relative;
620+
height: 100%;
621+
display: flex;
622+
align-items: center;
623+
margin-right: 2px;
624+
margin-top: 1px;
625+
}
626+
.hide {
627+
visibility: hidden;
628+
}
629+
617630
div#timertext {
618631
display: inline-block;
619632
padding: 2pt 0;

src-ui/js/list.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@
103103
localStorage.getItem("pzprv3_config:ui") || "{}"
104104
);
105105
setting.listsort = typename;
106-
localStorage.setItem("pzprv3_config:ui", JSON.stringify(setting));
106+
pzpr.util.store("pzprv3_config:ui", JSON.stringify(setting));
107107
self.apply_sort();
108108
},
109109

src-ui/js/ui/Boot.js

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,42 @@
1313
// ★boot() window.onload直後の処理
1414
//---------------------------------------------------------------------------
1515
pzpr.on("load", function boot() {
16-
if (importData()) {
17-
startPuzzle();
16+
var pzl;
17+
var storedGame = null;
18+
19+
if (pzpr.env.localStorageAvailable) {
20+
// If localStorage is available and autosave is enabled:
21+
// Get URL search hash and check localStorage to see if a board state is saved
22+
23+
// ui.menuconfig is not yet populated, so need to manually check
24+
var json_menu = localStorage.getItem("pzprv3_config:ui");
25+
if (json_menu && JSON.parse(json_menu)["autosave"]) {
26+
var key = "pzpr_" + getPuzzleString();
27+
storedGame = localStorage.getItem(key);
28+
}
29+
}
30+
31+
if (storedGame) {
32+
var valObject = JSON.parse(storedGame);
33+
pzl = importData(valObject.pzl);
1834
} else {
35+
pzl = importData();
36+
}
37+
if (!pzl) {
1938
setTimeout(boot, 0);
2039
}
40+
startPuzzle();
2141
});
2242

23-
function importData() {
43+
function importData(string) {
2444
if (!onload_pzl) {
2545
/* 1) 盤面複製・index.htmlからのファイル入力/Database入力か */
2646
/* 2) URL(?以降)をチェック */
27-
onload_pzl = importURL();
47+
if (!string) {
48+
onload_pzl = importURL();
49+
} else {
50+
onload_pzl = importFromString(string);
51+
}
2852

2953
/* 指定されたパズルがない場合はさようなら~ */
3054
if (!onload_pzl || !onload_pzl.pid) {
@@ -90,26 +114,75 @@
90114
//---------------------------------------------------------------------------
91115
function importURL() {
92116
/* index.htmlからURLが入力されていない場合は現在のURLの?以降をとってくる */
117+
var puzString = getPuzzleString();
118+
return importFromString(puzString);
119+
}
120+
//Splitting functionality from above for flexibility.
121+
122+
//Return the string associated with the puzzle
123+
function getPuzzleString() {
93124
var search = location.search;
94125
if (!search) {
95126
return null;
96127
}
97-
98-
/* 一旦先頭の?記号を取り除く */
99128
if (search.charAt(0) === "?") {
100-
search = search.substr(1);
129+
search = search.slice(1);
101130
}
102131

103132
while (search.match(/^(\w+)\=(\w+)\&(.*)/)) {
104133
onload_option[RegExp.$1] = RegExp.$2;
105134
search = RegExp.$3;
106135
}
136+
return search;
137+
}
138+
//Import from a puzzle string. This can come from the URL or from localStorage
139+
function importFromString(string) {
140+
if (!string) {
141+
return null;
142+
}
107143

108-
onload_pzv = search;
109-
var pzl = pzpr.parser.parseURL(search);
144+
onload_pzv = string;
145+
var pzl = pzpr.parser.parseURL(string);
110146
var startmode = pzl.mode || (!pzl.body ? "editor" : "player");
111147
onload_option.type = onload_option.type || startmode;
112148

113149
return pzl;
114150
}
151+
152+
//---------------------------------------------------------------------------
153+
// Functionality to support browser caching
154+
//---------------------------------------------------------------------------
155+
156+
//Save board state. Creates an entry in localStorage whose key is a 'pzpr_' identifier plus the current board state puzzle string.
157+
//Board state puzzle string is the same thing you get from duplicating the board state
158+
//Auto-exits if the correct setting is not set, so safe to call from anywhere without checking
159+
function saveBoardState() {
160+
if (!ui.menuconfig.get("autosave")) {
161+
return;
162+
}
163+
var key = "pzpr_" + getPuzzleString();
164+
var url = ui.puzzle.getURL(
165+
pzpr.parser.URL_PZPRFILE,
166+
ui.puzzle.playeronly ? "player" : "editor"
167+
);
168+
//Strip url to the last option. This is the "puzzle string" we want
169+
url = url.substring(url.indexOf("?") + 1); //Skip to the search parameters part of the url
170+
while (url.match(/^(\w+)\=(\w+)\&(.*)/)) {
171+
url = RegExp.$3;
172+
}
173+
//Add a time signifier so that we can sort and delete oldest if setting fails
174+
var valObject = {
175+
t: Date.now(),
176+
pzl: url
177+
// bufferToForceStorageLimitErrors: "0".repeat(1700000) //Include for testing to force out-of-storage errors
178+
};
179+
pzpr.util.store(key, JSON.stringify(valObject));
180+
}
181+
182+
//Events that trigger a board state save
183+
document.addEventListener("visibilitychange", function() {
184+
if (document.visibilityState === "hidden") {
185+
saveBoardState();
186+
}
187+
});
115188
})();

src-ui/js/ui/MenuConfig.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@
5050
volatile: true
5151
}); /* マウスの左右ボタンを反転する設定 */
5252

53+
//Autosave feature. Not on by default, but persists once enabled
54+
this.add("autosave", false, {
55+
volatile: false
56+
});
57+
5358
this.add("language", pzpr.lang, { option: ["en", "ja"] }); /* 言語設定 */
5459

5560
/* puzzle.configを一括で扱うため登録 */
@@ -264,11 +269,11 @@
264269
}
265270

266271
try {
267-
localStorage.setItem(
272+
pzpr.util.store(
268273
"pzprv3_config:puzzle",
269274
JSON.stringify(ui.puzzle.saveConfig())
270275
);
271-
localStorage.setItem("pzprv3_config:ui", JSON.stringify(this.getAll()));
276+
pzpr.util.store("pzprv3_config:ui", JSON.stringify(this.getAll()));
272277
} catch (ex) {
273278
console.warn(ex);
274279
}
@@ -336,6 +341,15 @@
336341
// config.configevent() 設定変更時の動作を記述する (modeはlistener.onModeChangeで変更)
337342
//---------------------------------------------------------------------------
338343
configevent: function(idname, newval) {
344+
//Need to set save icon visibility here to make sure it goes off before the early exit
345+
if (idname === "autosave") {
346+
var saveIcon = document.getElementById("saveicon");
347+
if (!!newval) {
348+
saveIcon.classList.remove("hide");
349+
} else {
350+
saveIcon.classList.add("hide");
351+
}
352+
}
339353
if (!ui.menuarea.menuitem) {
340354
return;
341355
}

src-ui/p.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ <h2 id="title2">読み込み中です...</h2>
9393
<li class="check" data-config="lattice"><span>__lattice.menu__</span></li>
9494
<li class="check" data-config="singlenum"><span>__singlenum.menu__</span></li>
9595
<li class="check" data-config="discolor"><span>__discolor__</span></li>
96+
<li class="check" data-config="autosave"><span>__autosave__</span></li>
9697
<li data-config="autocheck_mode"><span>__autocheck__</span>&nbsp;-&gt;<menu label="__autocheck__">
9798
<li class="child" data-value="off"><span>__autocheck.off__</span></li>
9899
<li class="child" data-value="guarded"><span>__autocheck.guarded__</span></li>
@@ -111,6 +112,7 @@ <h2 id="title2">読み込み中です...</h2>
111112
<li class="link"><a target="_blank" href="https://hosted.weblate.org/projects/pzprjs/">__translate__</a></li>
112113
<li class="link"><a target="_blank" href="https://github.com/robx/pzprjs/issues">__issues__</a></li>
113114
</menu></li>
115+
<div id="saveicon" class="hide">&#x1F4BE</div>
114116
</menu>
115117
<div id="usepanel" style="display:none;">
116118
<div class="config" data-config="mode">

src-ui/res/p.en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"autocheck.off": "Manual",
5858
"autocheck.guarded": "Automatic (guarded)",
5959
"autocheck.simple": "Automatic (always)",
60+
"autosave": "Autosave board state [beta]",
6061
"menu_help": "Help",
6162
"rules": "Rules",
6263
"about": "About puzz.link",

src-ui/res/p.ja.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"autocheck.off": "正答を自動で判定しない",
5757
"autocheck.guarded": "自動(guarded)",
5858
"autocheck.simple": "自動(常に)",
59+
"autosave": "Autosave board state [beta]",
5960
"menu_help": "ヘルプ",
6061
"rules": "ルール",
6162
"about": "puzz.linkについて",

src/pzpr/env.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,25 @@ pzpr.env = (function() {
6464
anchor_download:
6565
isbrowser && document.createElement("a").download !== void 0
6666
};
67+
//Taken directly from stackoverflow. Apparently this is the most broadly compatible version. https://stackoverflow.com/questions/16427636/check-if-localstorage-is-available
68+
var localStorageAvailable = (function() {
69+
var test = "test";
70+
try {
71+
localStorage.setItem(test, test);
72+
localStorage.removeItem(test);
73+
return true;
74+
} catch (e) {
75+
return false;
76+
}
77+
})();
6778

6879
return {
6980
bz: bz,
7081
OS: os,
7182
API: api,
7283
browser: isbrowser,
73-
node: pzpr.Candle.env.node
84+
node: pzpr.Candle.env.node,
85+
localStorageAvailable: localStorageAvailable
7486
};
7587
})();
7688

src/pzpr/util.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,39 @@
220220
}
221221
}
222222
return true;
223+
},
224+
225+
// "Safe" set to local storage. Catches quota exceeded errors and removes the oldest puzzles.
226+
// Separated here so that settings can be set safely as well
227+
store: function(key, value) {
228+
try {
229+
localStorage.setItem(key, value);
230+
} catch (e) {
231+
if (e.name === "QuotaExceededError") {
232+
//If storage was full: load all of the puzzles in localStorage, sort by least recent, and delete until saving is successful
233+
var pairs = [];
234+
for (var i = 0; i < localStorage.length; i++) {
235+
var lsKey = localStorage.key(i);
236+
var lsValue = JSON.parse(localStorage.getItem(lsKey));
237+
if (lsKey.indexOf("pzpr_") === 0) {
238+
pairs.push({ key: lsKey, value: lsValue });
239+
}
240+
}
241+
pairs.sort(function(a, b) {
242+
var ta = a.value.t;
243+
var tb = b.value.t;
244+
return ta - tb;
245+
});
246+
for (var i = 0; i < pairs.length; i++) {
247+
try {
248+
localStorage.setItem(key, value);
249+
break;
250+
} catch (e) {
251+
localStorage.removeItem(pairs[i].key);
252+
}
253+
}
254+
}
255+
}
223256
}
224257
};
225258
})();

0 commit comments

Comments
 (0)