aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile44
-rw-r--r--README78
-rw-r--r--c/data.h52
-rw-r--r--c/datalistview.cpp2
-rw-r--r--c/episodelistview.cpp8
-rw-r--r--c/main.cpp14
-rw-r--r--c/pl.cpp93
-rw-r--r--c/pl.h124
-rw-r--r--c/test.cpp64
-rw-r--r--c/util.h1
-rw-r--r--c/wcharptr.cpp64
-rw-r--r--c/wcharptr.h33
-rw-r--r--c/win.cpp25
-rw-r--r--c/win.h3
-rw-r--r--pl/atom_dcg.pl82
-rw-r--r--pl/cfg.pl76
-rw-r--r--pl/episode_data.pl196
-rw-r--r--pl/local_episodes.pl50
-rw-r--r--pl/track_episodes.pl112
19 files changed, 91 insertions, 1030 deletions
diff --git a/Makefile b/Makefile
index dcdbafe..610e81c 100644
--- a/Makefile
+++ b/Makefile
@@ -1,15 +1,25 @@
EXE = EpisodeBrowser.exe
-OBJ = b/datalistview.obj b/debug.obj b/data.obj b/episodelistview.obj b/ext.obj b/wcharptr.obj
-OBJ += b/win.obj b/layout.obj b/listview.obj b/pl.obj b/test.obj b/resource.obj
-PL = pl/cfg.pl pl/episode_data.pl pl/local_episodes.pl pl/track_episodes.pl
-
-CL = swipl-ld
-CLFLAGS = -DUNICODE -D_UNICODE
-CLFLAGS += -DDEBUG
-CLFLAGS += -cc-options,-std=c++17 -O0
-CLFLAGS += -ld-options,-mwindows,-static-libstdc++ -lcomctl32 -lwininet -llibxml2
-CLFLAGS += -IC:/msys64/mingw64/include/libxml2
-CLFLAGS += -Wall -Wextra -Wpedantic -Wno-missing-field-initializers -Wno-parentheses
+
+OBJ = b/datalistview.obj
+OBJ += b/data.obj
+OBJ += b/debug.obj
+OBJ += b/episodelistview.obj
+OBJ += b/ext.obj
+OBJ += b/layout.obj
+OBJ += b/listview.obj
+OBJ += b/main.obj
+OBJ += b/resource.obj
+OBJ += b/test.obj
+OBJ += b/wcharptr.obj
+OBJ += b/win.obj
+
+CC = gcc
+CFLAGS = -DUNICODE -D_UNICODE
+CFLAGS += -DDEBUG
+CFLAGS += -std=c++17 -mwindows -O0
+CFLAGS += -IC:/msys64/mingw64/include/libxml2
+CFLAGS += -Wall -Wextra -Wpedantic -Wno-missing-field-initializers -Wno-parentheses
+LDFLAGS += -lstdc++ -lcomctl32 -lwininet -llibxml2
all: showdeps b/$(EXE)
cp b/$(EXE) "C:\Users\John\Desktop\Delat"
@@ -17,20 +27,20 @@ all: showdeps b/$(EXE)
clean:
rm -fr b/$(EXE) b/*.obj
-TAGS: c/*.cpp c/*.h pl/*.pl
- etags --declarations -lc++ c/*.cpp c/*.h -lprolog pl/*.pl
+TAGS: c/*.cpp c/*.h
+ etags --declarations -lc++ c/*.cpp c/*.h
-b/$(EXE): Makefile c/main.cpp $(OBJ) $(PL)
- $(CL) $(CLFLAGS) -goal true -o $@ c/main.cpp $(OBJ) $(PL)
+b/$(EXE): Makefile $(OBJ)
+ $(CC) $(CFLAGS) -o $@ $(OBJ) $(LDFLAGS)
b/resource.obj: c/resource.h c/resource.rc c/application.manifest
windres -i c/resource.rc -o b/resource.obj
b/%.obj: c/%.cpp
- $(CL) -c $(CLFLAGS) -o $@ $<
+ $(CC) -c $(CFLAGS) -o $@ $<
s/%.s: c/%.cpp
- $(CL) $(CLFLAGS) -cc-options,-fverbose-asm,-masm=intel -S -o $@ $<
+ $(CC) $(CFLAGS) -fverbose-asm -masm=intel -S -o $@ $<
# showdeps prints a short summary of which dependencies have changed,
# causing which targets to be rebuilt. It is run by the `all' target
diff --git a/README b/README
index 1be0d2c..62e0a71 100644
--- a/README
+++ b/README
@@ -11,50 +11,52 @@ fetching episode information from the internet.
In its current form, it is designed specifically for Detective Conan.
-Backend
-~~~~~~~
-The fundamental features of Episode Browser, such as detecing and
-keeping track of local episodes, fetching information from the
-internet and interacting with the MPC-HC media player, are implemented
-in Prolog (see the pl folder).
-
-At the time of writing, the backend implementation is tightly coupled
-to the author's specific system. To be used for anything else than
-episodes of Detective Conan stored in a specific folder on the hard
-drive, the predicates defined in local_episodes.pl must be modified
-and the program recompiled. To fetch episode information from other
-sources than Detective Conan World, the episode_data.pl file must be
-modified.
-
-Frontend
-~~~~~~~~
-The graphical interface is implemented in C++ using the Win32 API (see
-the c folder). The source code is spread across a small number of
-files. In summary:
-
-main.cpp Entry point and initialization. Contains the main event handler.
-listview.* Creates and handles the shared aspects of the two list views.
-episodelistview.* Defines the interface to the episode list view.
-datalistview.* Defines the interface to the data list view.
-layout.* Handles the placement of child windows.
-pl.* Interface to Prolog backend.
-win.* Helpers for interacting with the Win32 API.
-wcharptr.* Defines WcharPtr, a class that owns a wchar_t*.
-util.h Simple utility functions.
-debug.* Helpers for debugging.
-
-The code operates under the assumption that "raw" pointers are
-non-owning -- i.e., their lifetime is managed by somebody else, such
-as Windows or Prolog. When strings need to be heap-allocated, like
-when converting from a narrow to a wide string, they are put within a
-WcharPtr object, which manages the lifetime of the string.
+Implementation
+~~~~~~~~~~~~~~
+Episode Browser is implemented in C++ using the Win32 API (see the c
+folder). The source code is spread across a number of files:
+
+>> Frontend <<
+
+main.cpp
+ Entry point and initialization. Contains the main event handler.
+listview.cpp,h
+ Defines the shared aspects of the two list views.
+episodelistview.cpp,h
+ Defines the interface to the episode list view.
+datalistview.cpp,h
+ Defines the interface to the data list view.
+layout.cpp,h
+ Handles the placement of child windows.
+resource.rc
+ Defines menu and dialog window resources.
+resource.h
+ Defines identifiers and macros for resources.
+
+>> Backend <<
+
+ext.cpp,h
+ Interacts with the external world (to open an episode).
+data.cpp,h
+ Defines the underlying data representation, as well as
+ functions to retrieve data from Detective Conan World.
+
+>> Miscellanea <<
+
+win.cpp,h
+ Helpers for interacting with the Win32 API.
+util.h
+ Utility functions.
+test.cpp,h
+ Simple test framework (work in progress).
+debug.cpp,h
+ Helpers for debugging.
Building
~~~~~~~~
Episode Browser is built with GNU Make. For the build process, the
following additional programs are required:
- - swipl-ld (included with SWI-Prolog)
- GCC/G++
- Perl
diff --git a/c/data.h b/c/data.h
index b974cec..1216359 100644
--- a/c/data.h
+++ b/c/data.h
@@ -4,9 +4,7 @@
#include <stdexcept>
#include <windows.h>
-#include "pl.h"
#include "util.h"
-#include "wcharptr.h"
#include "win.h"
/* Fetch data from the web. */
@@ -172,54 +170,4 @@ struct FileView
}
};
-inline int FromWeb(const int iEp, ElvDataA& e, DlvDataA& d) noexcept
-{
- WcharPtr title, wiki, date, source, hint;
- const int r = Pl("episode_data","fetch_episode_data",iEp,&title,&wiki,&date,&source,&hint);
- if (title) Wcscpy(e.title, title);
- if (wiki) Wcscpy(d.wiki, wiki);
- if (date) Wcscpy(d.date, date);
- if (source) Wcscpy(d.source, source);
- if (hint) Wcscpy(d.hint, hint);
- return r;
-}
-
-inline bool FromProlog(const int iEp, ElvDataA& e) noexcept
-{
- if (WcharPtr title; Pl("episode_data","episode_title",iEp,&title))
- Wcscpy(e.title, title);
- else
- return false;
-
- int rating;
- if (Pl("episode_data","episode_rating",iEp,&rating)) {
- e.rating = rating;
- Swprintf(e.sRating, L"%d", e.rating);
- }
-
- if (Pl("episode_data","tv_original",iEp))
- e.bTVOriginal = true;
-
- if (Pl("track_episodes","watched",iEp))
- e.bWatched = true;
-
- Swprintf(e.siEp, L"%d", iEp);
-
- return true;
-}
-
-inline void FromProlog(const int iEp, DlvDataA& d) noexcept
-{
- if (WcharPtr wiki; Pl("episode_data","episode_wiki",iEp,&wiki))
- Wcscpy(d.wiki, wiki);
- if (WcharPtr screenwriter; Pl("episode_data","episode_datum",iEp,"Screenwriter",&screenwriter))
- Wcscpy(d.screenwriter, screenwriter);
- if (WcharPtr date; Pl("episode_data","episode_datum",iEp,"Date",&date))
- Wcscpy(d.date, date);
- if (WcharPtr source; Pl("episode_data","episode_datum",iEp,"Source",&source))
- Wcscpy(d.source, source);
- if (WcharPtr hint; Pl("episode_data","episode_datum",iEp,"Hint",&hint))
- Wcscpy(d.hint, hint);
-}
-
#endif
diff --git a/c/datalistview.cpp b/c/datalistview.cpp
index a71a270..ec9592d 100644
--- a/c/datalistview.cpp
+++ b/c/datalistview.cpp
@@ -2,7 +2,6 @@
#include <stdio.h>
#include <windows.h>
#include <commctrl.h>
-#include <SWI-Prolog.h>
#include "resource.h"
#include "data.h"
@@ -10,7 +9,6 @@
#include "episodelistview.h"
#include "listview.h"
#include "layout.h"
-#include "wcharptr.h"
extern CfgA& g_cfg;
extern FileView<DlvDataA> g_fvDlv;
diff --git a/c/episodelistview.cpp b/c/episodelistview.cpp
index a76717a..72a8618 100644
--- a/c/episodelistview.cpp
+++ b/c/episodelistview.cpp
@@ -2,7 +2,6 @@
#include <vector>
#include <windows.h>
#include <commctrl.h>
-#include <SWI-Prolog.h>
#include "resource.h"
#include "data.h"
@@ -10,7 +9,6 @@
#include "episodelistview.h"
#include "ext.h"
#include "listview.h"
-#include "pl.h"
#include "util.h"
#include "win.h"
@@ -358,9 +356,6 @@ int CALLBACK EpisodeListView::SortProc(const LPARAM iItem1, const LPARAM iItem2,
void EpisodeListView::Update()
{
- if (!Pl("episode_data","ensure_episode_data"))
- return;
-
/* Save scrolling position. */
int iEpTop = 0;
{
@@ -371,9 +366,8 @@ void EpisodeListView::Update()
/* Save selected episodes. */
int iItemMark;
- static std::vector<int> vEpSel;
+ std::vector<int> vEpSel;
{
- vEpSel.clear();
LVITEM lvi = {LVIF_PARAM, -1};
while (FindNextItem(&lvi, LVNI_SELECTED))
vEpSel.push_back(lvi.lParam);
diff --git a/c/main.cpp b/c/main.cpp
index 244d1fe..9919200 100644
--- a/c/main.cpp
+++ b/c/main.cpp
@@ -2,7 +2,6 @@
#include <stdexcept>
#include <windows.h>
#include <commctrl.h>
-#include <SWI-Prolog.h>
#include <libxml/xmlversion.h>
#include "debug.h"
@@ -10,7 +9,6 @@
#include "datalistview.h"
#include "episodelistview.h"
#include "layout.h"
-#include "pl.h"
#include "test.h"
#include "util.h"
@@ -86,13 +84,6 @@ int WINAPI WinMain(const HINSTANCE hInstance, const HINSTANCE, char* const, cons
setbuf(stdout, nullptr);
LIBXML_TEST_VERSION;
- /* Initialize Prolog. */
- const char* argv[] = {"EpisodeBrowser", nullptr};
- if (!PL_initialise(1, const_cast<char**>(argv)))
- throw std::runtime_error("Could not initialize Prolog.");
- if (!Pl("track_episodes","attach") || !Pl("episode_data","attach"))
- throw std::runtime_error("Could not attach databases.");
-
INITCOMMONCONTROLSEX icc;
icc.dwSize = sizeof(icc);
icc.dwICC = ICC_WIN95_CLASSES;
@@ -134,7 +125,7 @@ int WINAPI WinMain(const HINSTANCE hInstance, const HINSTANCE, char* const, cons
ShowWindow(hWnd, nCmdShow);
/* Populate episode list view. */
- Pl("track_episodes","update_tracked_episodes");
+ /* TODO: Update tracked episodes. */
g_elv->Update();
g_elv->RestoreFocus();
@@ -149,7 +140,6 @@ int WINAPI WinMain(const HINSTANCE hInstance, const HINSTANCE, char* const, cons
DispatchMessage(&msg);
}
- PL_halt(0);
return 0;
}
@@ -274,7 +264,7 @@ LRESULT CALLBACK HandleMsg(const HWND hWnd, const UINT uMsg, const WPARAM wParam
g_hWndFocus = GetFocus();
else {
SetFocus(g_hWndFocus);
- Pl("track_episodes","update_tracked_episodes");
+ /* TODO: Update tracked episodes. */
g_elv->Redraw();
}
return 0;
diff --git a/c/pl.cpp b/c/pl.cpp
deleted file mode 100644
index bab2f31..0000000
--- a/c/pl.cpp
+++ /dev/null
@@ -1,93 +0,0 @@
-#include <windows.h>
-#include <SWI-Prolog.h>
-
-#include "pl.h"
-
-Frame::Frame()
-{
- m_f = PL_open_foreign_frame();
-}
-
-Frame::~Frame()
-{
- PL_close_foreign_frame(m_f);
-}
-
-void Frame::Close()
-{
- PL_close_foreign_frame(m_f);
-}
-
-void Frame::Discard()
-{
- PL_discard_foreign_frame(m_f);
-}
-
-void Frame::Rewind()
-{
- PL_rewind_foreign_frame(m_f);
-}
-
-Mark::Mark()
-{
- PL_mark_string_buffers(&m_m);
-}
-
-Mark::~Mark()
-{
- PL_release_string_buffers_from_mark(m_m);
-}
-
-Query::Query(const module_t ctx, const predicate_t p, const term_t t0)
-{
- m_q = PL_open_query(ctx, PL_Q_CATCH_EXCEPTION, p, t0);
-}
-
-Query::~Query()
-{
- PL_cut_query(m_q);
-}
-
-int Query::Cut(std::nothrow_t)
-{
- return PL_cut_query(m_q);
-}
-int Query::Cut()
-{
- if (PL_cut_query(m_q)) return 1;
- if (const term_t t = PL_exception(m_q)) throw t;
- return 0;
-}
-
-int Query::Close(std::nothrow_t)
-{
- return PL_close_query(m_q);
-}
-
-int Query::Close()
-{
- if (PL_close_query(m_q)) return 1;
- if (const term_t t = PL_exception(m_q)) throw t;
- return 0;
-}
-
-int Query::NextSolution(std::nothrow_t)
-{
- return PL_next_solution(m_q);
-}
-
-int Query::NextSolution()
-{
- if (PL_next_solution(m_q)) return 1;
- if (const term_t t = PL_exception(m_q)) throw t;
- return 0;
-}
-
-WcharPtr PlString(const term_t t, const int flags)
-{
- char* s;
- if (PL_get_chars(t, &s, flags))
- return {WcharPtr::FromNarrow(s)};
- else
- return {};
-}
diff --git a/c/pl.h b/c/pl.h
deleted file mode 100644
index 2992e86..0000000
--- a/c/pl.h
+++ /dev/null
@@ -1,124 +0,0 @@
-#ifndef PL_H
-#define PL_H
-
-#include <string>
-#include <windows.h>
-#include <SWI-Prolog.h>
-
-#include "wcharptr.h"
-
-/* Convert Prolog term to wide characters. */
-WcharPtr PlString(const term_t t, const int flags = CVT_WRITE);
-
-/* Wrapper for opening and closing Prolog foreign frames. */
-struct Frame
-{
- Frame();
- ~Frame();
- void Close();
- /* Close foreign frame AND invalidate associated data. */
- void Discard();
- void Rewind();
-private:
- fid_t m_f;
-};
-
-/* Wrapper for marking and releasing strings on the Prolog stack. */
-struct Mark
-{
- Mark();
- ~Mark();
-private:
- buf_mark_t m_m;
-};
-
-/* Wrapper for opening, solving and cutting Prolog query. */
-struct Query
-{
- Query(module_t ctx, predicate_t p, term_t t0);
- ~Query();
- int Cut();
- int Cut(std::nothrow_t);
- /* Cut query AND invalidate associated data. */
- int Close();
- int Close(std::nothrow_t);
- int NextSolution();
- int NextSolution(std::nothrow_t);
-private:
- qid_t m_q;
-};
-
-/* Polymorphic aliases for PL_put_*, PL_get_*. */
-inline int PlPut(term_t t, int x) { return PL_put_integer(t, x); }
-inline int PlPut(term_t t, long x) { return PL_put_integer(t, x); }
-inline int PlPut(term_t t, long long x) { return PL_put_int64(t, x); }
-inline int PlPut(term_t t, atom_t x) { return PL_put_atom(t, x); }
-inline int PlPut(term_t t, char* x) { return PL_put_atom_chars(t, x); }
-inline int PlPut(term_t t, const char* x) { return PL_put_atom_chars(t, x); }
-inline int PlPut(term_t, int*) { return -1; }
-inline int PlPut(term_t, long*) { return -1; }
-inline int PlPut(term_t, long long*) { return -1; }
-inline int PlPut(term_t, atom_t*) { return -1; }
-inline int PlPut(term_t, char**) { return -1; }
-inline int PlPut(term_t, WcharPtr*) { return -1; }
-
-inline int PlGet(term_t, int) { return -1; }
-inline int PlGet(term_t, long) { return -1; }
-inline int PlGet(term_t, long long) { return -1; }
-inline int PlGet(term_t, atom_t) { return -1; }
-inline int PlGet(term_t, char*) { return -1; }
-inline int PlGet(term_t, const char*) { return -1; }
-inline int PlGet(term_t t, int* x) { return PL_get_integer(t, x); }
-inline int PlGet(term_t t, long* x) { return PL_get_long(t, x); }
-inline int PlGet(term_t t, long long* x) { return PL_get_int64(t, x); }
-inline int PlGet(term_t t, atom_t* x) { return PL_get_atom(t, x); }
-inline int PlGet(term_t t, char** x) { return PL_get_atom_chars(t, x); }
-inline int PlGet(term_t t, WcharPtr* x)
-{
- Mark m;
- char* s;
- if (!PlGet(t, &s)) return 0;
- *x = WcharPtr::FromNarrow(s);
- return 1;
-}
-
-/* Put in or get from a term reference an arbitrary number of values,
- * returning false if any value could not be put/got. */
-template <typename = void>
-inline bool PlPutN(term_t) { return true; }
-template <typename T, typename... U>
-inline bool PlPutN(term_t t, T x, U... xs) { return PlPut(t, x) && PlPutN(t+1, xs...); }
-
-template <typename = void>
-inline bool PlGetN(term_t) { return true; }
-template <typename T, typename... U>
-inline bool PlGetN(term_t t, T x, U... xs) { return PlGet(t, x) && PlGetN(t+1, xs...); }
-
-/* Call Prolog predicate. */
-template <bool Except = false, typename... T>
-int Pl(const char* const mod, const char* const pred, T... xs)
-{
- Frame f;
- const term_t t = PL_new_term_refs(sizeof...(T));
-
- if (!PlPutN(t, xs...)) return 0;
- Query q(nullptr, PL_predicate(pred, sizeof...(T), mod), t);
-
- if constexpr (Except) {
- if (!q.NextSolution()) return 0;
- } else {
- if (!q.NextSolution(std::nothrow)) return 0;
- }
-
- if (!PlGetN(t, xs...)) return 0;
- return 1;
-}
-
-/* Call Prolog predicate, propagating Prolog exceptions. */
-template <typename... T>
-int Plx(const char* const mod, const char* const pred, T... xs)
-{
- return Pl<true>(mod, pred, xs...);
-}
-
-#endif
diff --git a/c/test.cpp b/c/test.cpp
index 0f59af4..6e291c7 100644
--- a/c/test.cpp
+++ b/c/test.cpp
@@ -4,7 +4,6 @@
#include "data.h"
#include "episodelistview.h"
#include "ext.h"
-#include "pl.h"
#include "util.h"
#include "win.h"
@@ -36,9 +35,9 @@ TESTS
TEST(IO)
{
- ElvDataA e1_0, e2_0;
- FromProlog(6, e1_0);
- FromProlog(10, e2_0);
+ extern FileView<ElvDataA> g_fvElv;
+ ElvDataA& e1_0 = g_fvElv.At(5);
+ ElvDataA& e2_0 = g_fvElv.At(9);
/* Write two ElvDataA structs to disk. */
{
@@ -89,63 +88,10 @@ TESTS
//DeleteFile(L"tmp.dat");
}
- TEST(MigrateElvDataFromPrologToDisk)
- {
- int cEp;
- if (!Pl("episode_data","episode_count",&cEp))
- return;
-
- {
- FileView<ElvDataA> fv(L"tmp.dat", g_cfg.cEp+128u);
- ElvDataA* p = fv;
-
- for (int iEp = 1; iEp <= cEp; iEp++) {
- ElvDataA e;
- FromProlog(iEp, e);
- memcpy(p, &e, sizeof(e));
- p++;
- }
- }
- {
- FileView<ElvDataA> fv(L"tmp.dat", g_cfg.cEp+128u);
- ElvDataA& e = fv.At(9);
- if (wcscmp(e.title, L"Pro Soccer Player Blackmail Case") != 0)
- FAIL("title is not correct");
- }
- //DeleteFile(L"tmp.dat");
- }
-
- TEST(MigrateDlvDataFromPrologToDisk)
- {
- int cEp;
- if (!Pl("episode_data","episode_count",&cEp))
- return;
-
- {
- FileView<DlvDataA> fv(L"tmp.dat", g_cfg.cEp+128u);
- DlvDataA* p = fv;
-
- for (int iEp = 1; iEp <= cEp; iEp++) {
- DlvDataA d;
- FromProlog(iEp, d);
- memcpy(p, &d, sizeof(d));
- p++;
- }
- }
- {
- FileView<DlvDataA> fv(L"tmp.dat", g_cfg.cEp+128u);
- DlvDataA& e = fv.At(9);
- if (wcscmp(e.date, L"March 11, 1996") != 0)
- FAIL("date is not correct");
- }
- //DeleteFile(L"tmp.dat");
- }
-
// TEST(MigrateCfg)
// {
-// FileView<CfgA> fva(L"cfga.dat", 1);
// FileView<CfgB> fvb = FileView<CfgB>::Initialized(L"cfgb.dat", 1);
-// CfgA* a = fva+0;
+// CfgA* a = &g_cfg;
// CfgB* b = fvb+0;
// #define CPY(member) b->member = a->member;
@@ -172,8 +118,6 @@ int RunTests()
const Test tests[] = {
StrcpyWithSmallerDestination(),
//IO(),
- //MigrateElvDataFromPrologToDisk(),
- //MigrateDlvDataFromPrologToDisk(),
//MigrateCfg(),
};
diff --git a/c/util.h b/c/util.h
index 7f704df..729e64d 100644
--- a/c/util.h
+++ b/c/util.h
@@ -4,7 +4,6 @@
#include <cstring>
#include <memory>
#include <string>
-#include <SWI-Prolog.h>
#include <windows.h>
#define CONCAT_IMPL(a, b) a##b
diff --git a/c/wcharptr.cpp b/c/wcharptr.cpp
deleted file mode 100644
index f659767..0000000
--- a/c/wcharptr.cpp
+++ /dev/null
@@ -1,64 +0,0 @@
-#include <utility>
-#include <windows.h>
-
-#include "wcharptr.h"
-#include "win.h"
-
-WcharPtr WcharPtr::FromNarrow(const char* const src, const int cp)
-{
- int cchNarrow = strlen(src)+1;
- int cchWide = MultiByteToWideChar(cp, 0, src, cchNarrow, nullptr, 0);
- wchar_t* dst = new wchar_t[cchWide];
- if (!MultiByteToWideChar(cp, 0, src, cchNarrow, dst, cchWide)) {
- delete dst;
- throw Win32Error();
- }
- return dst;
-}
-
-WcharPtr WcharPtr::Copy(const wchar_t* const src)
-{
- const int cch = wcslen(src)+1;
- wchar_t* dst = new wchar_t[cch];
- memcpy(dst, src, cch*sizeof(wchar_t));
- return dst;
-}
-
-WcharPtr::WcharPtr() noexcept {}
-
-WcharPtr::~WcharPtr() noexcept
-{
- delete m_p;
-}
-
-WcharPtr::operator wchar_t*() noexcept
-{
- return m_p;
-}
-
-WcharPtr::WcharPtr(wchar_t* const s) noexcept : m_p(s) {}
-
-WcharPtr& WcharPtr::operator=(wchar_t* const s) noexcept
-{
- if (m_p != s) {
- delete m_p;
- m_p = s;
- }
- return *this;
-}
-
-WcharPtr::WcharPtr(WcharPtr&& other) noexcept
- : m_p(std::exchange(other.m_p, nullptr)) {}
-
-WcharPtr& WcharPtr::operator=(WcharPtr&& other) noexcept
-{
- std::swap(m_p, other.m_p);
- return *this;
-}
-
-wchar_t* WcharPtr::Release() noexcept
-{
- wchar_t* const p = m_p;
- m_p = nullptr;
- return p;
-}
diff --git a/c/wcharptr.h b/c/wcharptr.h
deleted file mode 100644
index 2267017..0000000
--- a/c/wcharptr.h
+++ /dev/null
@@ -1,33 +0,0 @@
-#ifndef WCHARPTR_H
-#define WCHARPTR_H
-
-#include <windows.h>
-
-/* WcharPtr: Simple wrapper for wide C strings. */
-struct WcharPtr
-{
- /* Named copy constructors (expensive). */
- static WcharPtr FromNarrow(const char* buf, int cp = CP_UTF8);
- static WcharPtr Copy(const wchar_t* s);
-
- /* Non-explicit copies are disabled. */
- WcharPtr(WcharPtr& other) = delete;
- WcharPtr& operator=(WcharPtr& other) = delete;
-
- WcharPtr() noexcept;
- ~WcharPtr();
- operator wchar_t*() noexcept;
-
- WcharPtr(wchar_t* s) noexcept;
- WcharPtr& operator=(wchar_t* s) noexcept;
-
- WcharPtr(WcharPtr&& other) noexcept;
- WcharPtr& operator=(WcharPtr&& other) noexcept;
-
- /* Return pointer, releasing ownership. */
- wchar_t *Release() noexcept;
-private:
- wchar_t* m_p = nullptr;
-};
-
-#endif
diff --git a/c/win.cpp b/c/win.cpp
index ba42508..43744dd 100644
--- a/c/win.cpp
+++ b/c/win.cpp
@@ -1,13 +1,21 @@
#include <utility>
#include <string>
-#include <SWI-Prolog.h>
+#include <string_view>
#include <windows.h>
#include <wininet.h>
-#include "pl.h"
#include "util.h"
#include "win.h"
-#include "wcharptr.h"
+
+std::wstring WideFromNarrow(const std::string_view src, const int cp)
+{
+ int cchNarrow = src.length()+1;
+ int cchWide = MultiByteToWideChar(cp, 0, src.data(), cchNarrow, nullptr, 0);
+ std::wstring dst(cchWide, 0);
+ if (!MultiByteToWideChar(cp, 0, src.data(), cchNarrow, dst.data(), cchWide))
+ throw Win32Error();
+ return dst;
+}
void WithNextWindow(void (*proc)(HWND))
{
@@ -87,19 +95,14 @@ void ShowException(const wchar_t* const fmt, const wchar_t* const title, const U
{
try {
std::rethrow_exception(std::current_exception());
- } catch (const term_t& t) {
- WcharPtr what = PlString(t);
- std::wstring msg(wcslen(fmt)+wcslen(what), 0);
- Swprintf(msg, fmt, static_cast<wchar_t*>(what));
- EBMessageBox(msg, title, uType);
} catch (const WideException& e) {
std::wstring msg(wcslen(fmt)+wcslen(e.What()), 0);
Swprintf(msg, fmt, e.What());
EBMessageBox(msg, title, uType);
} catch (const std::exception& e) {
- auto what = WcharPtr::FromNarrow(e.what());
- std::wstring msg(wcslen(fmt)+wcslen(what), 0);
- Swprintf(msg, fmt, static_cast<wchar_t*>(what));
+ std::wstring what = WideFromNarrow(e.what());
+ std::wstring msg(wcslen(fmt)+what.length(), 0);
+ Swprintf(msg, fmt, what.c_str());
EBMessageBox(msg, title, uType);
} catch (...) {
const wchar_t* what = L"an unknown error occurred";
diff --git a/c/win.h b/c/win.h
index 834aaec..8329f68 100644
--- a/c/win.h
+++ b/c/win.h
@@ -6,6 +6,9 @@
#include <windows.h>
#include <commctrl.h>
+/* Convert narrow to wide string. */
+std::wstring WideFromNarrow(const std::string_view src, const int cp = CP_UTF8);
+
/* Run given procedure at creation of next window. */
void WithNextWindow(void (*proc)(HWND));
diff --git a/pl/atom_dcg.pl b/pl/atom_dcg.pl
deleted file mode 100644
index c5b9bfc..0000000
--- a/pl/atom_dcg.pl
+++ /dev/null
@@ -1,82 +0,0 @@
-:- module(atom_dcg, [compound_atom_codes/2]).
-
-% with_atoms/1-9 automatically convert atoms for the caller to codes
-% for the callee, primarily to make atoms easier to use with
-% definite-clause grammars. atom_phrase/2 is defined as a shortcut.
-
-% with_codes/1-9, conversely, convert codes for the caller to atoms
-% for the callee.
-
-:- meta_predicate user:with_atoms(0).
-:- meta_predicate user:with_atoms(0,0).
-:- meta_predicate user:with_atoms(0,0,0).
-:- meta_predicate user:with_atoms(0,0,0,0).
-:- meta_predicate user:with_atoms(0,0,0,0,0).
-:- meta_predicate user:with_atoms(0,0,0,0,0,0).
-:- meta_predicate user:with_atoms(0,0,0,0,0,0,0).
-:- meta_predicate user:with_atoms(0,0,0,0,0,0,0,0).
-:- meta_predicate user:with_atoms(0,0,0,0,0,0,0,0,0).
-user:with_atoms(A) :- atom_dcg:compound_atom_codes(A, A1), call(A1).
-user:with_atoms(A, B) :- maplist(user:with_atoms, [A,B]).
-user:with_atoms(A, B, C) :- maplist(user:with_atoms, [A,B,C]).
-user:with_atoms(A, B, C, D) :- maplist(user:with_atoms, [A,B,C,D]).
-user:with_atoms(A, B, C, D, E) :- maplist(user:with_atoms, [A,B,C,D,E]).
-user:with_atoms(A, B, C, D, E, F) :- maplist(user:with_atoms, [A,B,C,D,E,F]).
-user:with_atoms(A, B, C, D, E, F, G) :- maplist(user:with_atoms, [A,B,C,D,E,F,G]).
-user:with_atoms(A, B, C, D, E, F, G, H) :- maplist(user:with_atoms, [A,B,C,D,E,F,G,H]).
-user:with_atoms(A, B, C, D, E, F, G, H, I) :- maplist(user:with_atoms, [A,B,C,D,E,F,G,H,I]).
-
-:- meta_predicate user:with_codes(0).
-:- meta_predicate user:with_codes(0,0).
-:- meta_predicate user:with_codes(0,0,0).
-:- meta_predicate user:with_codes(0,0,0,0).
-:- meta_predicate user:with_codes(0,0,0,0,0).
-:- meta_predicate user:with_codes(0,0,0,0,0,0).
-:- meta_predicate user:with_codes(0,0,0,0,0,0,0).
-:- meta_predicate user:with_codes(0,0,0,0,0,0,0,0).
-:- meta_predicate user:with_codes(0,0,0,0,0,0,0,0,0).
-user:with_codes(A) :- atom_dcg:compound_atom_codes(A1, A), call(A1).
-user:with_codes(A, B) :- maplist(user:with_codes, [A,B]).
-user:with_codes(A, B, C) :- maplist(user:with_codes, [A,B,C]).
-user:with_codes(A, B, C, D) :- maplist(user:with_codes, [A,B,C,D]).
-user:with_codes(A, B, C, D, E) :- maplist(user:with_codes, [A,B,C,D,E]).
-user:with_codes(A, B, C, D, E, F) :- maplist(user:with_codes, [A,B,C,D,E,F]).
-user:with_codes(A, B, C, D, E, F, G) :- maplist(user:with_codes, [A,B,C,D,E,F,G]).
-user:with_codes(A, B, C, D, E, F, G, H) :- maplist(user:with_codes, [A,B,C,D,E,F,G,H]).
-user:with_codes(A, B, C, D, E, F, G, H, I) :- maplist(user:with_codes, [A,B,C,D,E,F,G,H,I]).
-
-:- meta_predicate user:atom_phrase(2, ?).
-user:atom_phrase(G, A) :-
- atom_dcg:compound_atom_codes(G, G1),
- ( var(A)
- -> phrase(G1, C),
- atom_codes(A, C)
- ; atom_codes(A, C),
- phrase(G1, C)
- ).
-
-compound_atom_codes(Module:G, Module:G1) :- !, compound_atom_codes(G, G1).
-compound_atom_codes(G, G1) :-
- ( nonvar(G)
- -> G =.. [F|Args0],
- maplist(maybe_atom_codes, Args0, Args),
- G1 =.. [F|Args]
- ; G1 =.. [F|Args],
- maplist(maybe_atom_codes, Args0, Args),
- G =.. [F|Args0]
- ).
-
-maybe_atom_codes(A, C) :-
- (compound(A) ; compound(C)), !,
- compound_atom_codes(A, C).
-
-maybe_atom_codes(A, C) :-
- nonvar(A), !,
- ( atom(A)
- -> atom_codes(A, C)
- ; C = A
- ).
-
-maybe_atom_codes(A, C) :-
- var(A), !,
- when((ground(A) ; ground(C)), catch(atom_codes(A, C), _, A = C)).
diff --git a/pl/cfg.pl b/pl/cfg.pl
deleted file mode 100644
index 297d426..0000000
--- a/pl/cfg.pl
+++ /dev/null
@@ -1,76 +0,0 @@
-:- module(cfg, [set_glob/1,
- get_glob/1,
- set_root/1,
- get_root/1,
- set_url/1,
- get_url/1]).
-
-:- use_module(library(registry)).
-
-set_key(Key, Value) :-
- registry_set_key(current_user/software/'JohnAJ'/'EpisodeBrowser'/'Settings', Key, Value).
-get_key(Key, Value) :-
- catch(registry_get_key(current_user/software/'JohnAJ'/'EpisodeBrowser'/'Settings', Key, Value),
- _,
- fail).
-
-set_root(V) :- set_key('EpisodeRoot', V).
-get_root(V) :-
- get_key('EpisodeRoot', V0),
- re_replace('\\\\', '/', V0, V1),
- atom_string(V, V1).
-
-set_glob(V) :- set_key('EpisodeGlob', V).
-get_glob(V) :-
- ( get_key('EpisodeGlob', V), !
- ; V = '*/*.*'
- ).
-
-set_url(V) :- set_key('UrlPrefix', V).
-get_url(V) :-
- ( get_key('UrlPrefix', V), !
- ; V = 'https://animixplay.to/v1/detective-conan/ep'
- ).
-
-set_view_watched(V) :- set_key('ViewWatched', V).
-get_view_watched(V) :-
- ( get_key('ViewWatched', V),
- integer(V), !
- ; V = 1
- ).
-
-set_view_tv_original(V) :- set_key('ViewTVOriginal', V).
-get_view_tv_original(V) :-
- ( get_key('ViewTVOriginal', V),
- integer(V), !
- ; V = 1
- ).
-
-set_limit_screenwriter(V) :- set_key('LimitScreenwriter', V).
-get_limit_screenwriter(V) :-
- ( get_key('LimitScreenwriter', V),
- atom(V), !
- ; V = ""
- ).
-
-set_sort(V) :- W is V + 2147483647, set_key('Sort', W).
-get_sort(V) :-
- ( get_key('Sort', W),
- integer(W), !,
- V is W - 2147483647
- ; V = 1
- ).
-
-set_focus(V) :- set_key('Focus', V).
-get_focus(V) :-
- ( get_key('Focus', V),
- integer(V), !
- ; V = 1
- ).
-
-set_dlv_height(V) :- set_key('DlvHeight', V).
-get_dlv_height(V) :-
- ( get_key('DlvHeight', V),
- integer(V), !
- ; V = 0
- ).
diff --git a/pl/episode_data.pl b/pl/episode_data.pl
deleted file mode 100644
index 5b83546..0000000
--- a/pl/episode_data.pl
+++ /dev/null
@@ -1,196 +0,0 @@
-:- module(episode_data, [ensure_episode_data/0,
- update_episode_data/0,
- update_screenwriters/0,
- retract_episode/1,
- episode_count/1,
- rate_episode/2,
- episode_rating/2,
- tv_original/1,
- thread_running/1,
- thread_exception/2]).
-
-:- use_module(library(clpfd)).
-:- use_module(library(dcg/basics)).
-:- use_module(library(http/http_open)).
-:- use_module(library(sgml)).
-:- use_module(library(xpath)).
-:- use_module(library(persistency)).
-:- use_module(atom_dcg).
-
-:- persistent episode_title(episode:integer, title:atom).
-:- persistent episode_wiki(episode:integer, wiki:atom).
-:- persistent episode_datum(episode:integer, key:atom, value:atom).
-:- persistent episode_rating(episode:integer, rating:integer).
-
-attach :-
- absolute_file_name('episode_data.db', F, [access(write)]),
- db_attach(F, []).
-
-detach :-
- db_detach.
-
-ensure_episode_data :- episode_title(_, _), !.
-ensure_episode_data :- update_episode_data.
-
-retract_episode(Ep) :-
- ( episode_title(Ep, _)
- -> retractall_episode_title(Ep, _)
- ; true
- ),
- ( episode_datum(Ep, 'Hint', _)
- -> retractall_episode_datum(Ep, 'Hint', _)
- ; true
- ).
-
-episode_count(N) :-
- setof(E, T^episode_title(E,T), Es),
- last(Es, N).
-
-rate_episode(Ep, 0) :-
- ( episode_rating(Ep, _)
- -> retractall_episode_rating(Ep, _)
- ; true
- ),
- !.
-
-rate_episode(Ep, R) :-
- dif(R, 0),
- ( episode_rating(Ep, R)
- -> true
- ; ( episode_rating(Ep, _)
- -> retractall_episode_rating(Ep, _)
- ; true
- ),
- assert_episode_rating(Ep, R)
- ).
-
-open_episode_wiki(Ep) :-
- episode_wiki(Ep, W),
- win_shell(open, W).
-
-tv_original(Ep) :- episode_datum(Ep, 'Source', 'TV Original').
-
-% Remote data retrieval.
-
-absolute_url(R) --> "https://www.detectiveconanworld.com", R.
-
-thread_running(T) :- thread_property(T, status(running)).
-thread_exception(T, E) :- thread_property(T, status(exception(E))).
-
-update_episode_data :-
- findall(Ep-Info,
- (fetch_episode_info(Ep, Info)),
- Set),
- maplist(set_episode_info, Set).
-
-set_episode_info(Ep-Info) :-
- maplist(set_episode_datum(Ep), Info).
-
-set_episode_datum(Ep, 'Title'-Title) :- !,
- maybe_assert_episode_title(Ep, Title).
-set_episode_datum(Ep, 'Wiki'-W) :- !,
- maybe_assert_episode_wiki(Ep, W).
-set_episode_datum(Ep, Key-Value) :-
- maybe_assert_episode_datum(Ep, Key, Value).
-
-maybe_assert_episode_title(Ep, Title) :-
- ( episode_title(Ep, Title), !
- ; assert_episode_title(Ep, Title)
- ).
-
-maybe_assert_episode_wiki(Ep, W) :-
- ( episode_wiki(Ep, W), !
- ; assert_episode_wiki(Ep, W)
- ).
-
-maybe_assert_episode_datum(Ep, Key, Value) :-
- ( episode_datum(Ep, Key, Value), !
- ; ( episode_datum(Ep, Key, _)
- -> retract_episode_datum(Ep, Key, _)
- ; true
- ),
- assert_episode_datum(Ep, Key, Value)
- ).
-
-episode_number(Ep) --> integer(Ep).
-episode_number(Ep) --> integer(Ep), "WPS", integer(_).
-
-% Temporary helper.
-fetch_episode_data(Ep, Title, W, Date, Source, Hint) :-
- fetch_episode_info(Ep, ['Title'-Title, 'Wiki'-W, 'Date'-Date, 'Source'-Source, 'Hint'-Hint]).
-
-fetch_episode_info(Ep, ['Title'-Title, 'Wiki'-W, 'Date'-Date, 'Source'-Source, 'Hint'-Hint]) :-
- cached_html('https://www.detectiveconanworld.com/wiki/Anime', H),
- xpath(H, //tr(td(index(3),@style='background:#f2fde9;')), R),
- xpath(R, td(index(1),normalize_space), Ep0),
- atom_phrase(episode_number(Ep), Ep0),
- xpath(R, td(index(3),normalize_space), Title),
- xpath(R, td(index(3))/a(@href), W0),
- atom_phrase(absolute_url(W0), W),
- xpath(R, td(index(4),normalize_space), Date),
- xpath(R, td(index(7),normalize_space), Source0),
- re_replace('\\(([0-9])', ' (\\1', Source0, Source1),
- atom_string(Source, Source1),
- xpath(R, td(index(8),normalize_space), Hint).
-
-update_screenwriters :-
- findall(Ep-Name,
- (maybe_fetch_screenwriter_episode(Name, Ep)),
- Set),
- maplist(set_episode_screenwriter, Set).
-
-set_episode_screenwriter(Ep-Name) :-
- maybe_assert_episode_datum(Ep, 'Screenwriter', Name).
-
-maybe_fetch_screenwriter_episode(Name, Ep) :-
- \+ episode_datum(Ep, 'Screenwriter', Ep),
- fetch_screenwriter_episode(Name, Ep).
-
-fetch_screenwriter(Name) :-
- cached_html('https://www.detectiveconanworld.com/wiki/Category:Screenplay_writers', H),
- xpath(H, //'div'(@id='mw-pages')//a, A),
- xpath(A, /self(normalize_space), Name).
-
-fetch_screenwriter_url(Name, U) :-
- cached_html('https://www.detectiveconanworld.com/wiki/Category:Screenplay_writers', H),
- xpath(H, //'div'(@id='mw-pages')//a, A),
- xpath(A, /self(normalize_space), Name),
- xpath(A, /self(@href), U).
-
-screenwriter_episode(Ep) --> string(_), "(Episode ", integer(Ep), ")".
-screenwriter_episode(Ep) --> string(_), "(Episodes ", integer(Ep1), "-", integer(Ep2), ")",
- { between(Ep1, Ep2, Ep) }.
-screenwriter_episode(Ep) --> string(_), "(Episodes ", integer(Ep1), "-", integer(Ep2), " only)",
- { between(Ep1, Ep2, Ep) }.
-
-fetch_screenwriter_episode(Name, Ep) :-
- fetch_screenwriter_url(Name, U0),
- atom_phrase(absolute_url(U0), U),
- cached_html(U, H),
- xpath(H, //div(@id='mw-content-text')//li, L),
- xpath(L, /self(normalize_space), T),
- atom_phrase(screenwriter_episode(Ep), T).
-
-fetch_html(U, H) :-
- catch(http_load_html(U, H), _, fail),
- !,
- nb_setval(U, H).
-
-cached_html(U, H) :- nb_current(U, H), !.
-cached_html(U, H) :- fetch_html(U, H).
-
-http_load_html(URL, DOM) :-
- setup_call_cleanup(http_open(URL, In,
- [ timeout(60)
- ]),
- ( dtd(html, DTD),
- load_structure(stream(In),
- DOM,
- [ dtd(DTD),
- dialect(sgml),
- shorttag(false),
- max_errors(-1),
- syntax_errors(quiet)
- ])
- ),
- close(In)).
diff --git a/pl/local_episodes.pl b/pl/local_episodes.pl
deleted file mode 100644
index 68809a7..0000000
--- a/pl/local_episodes.pl
+++ /dev/null
@@ -1,50 +0,0 @@
-:- module(local_episodes, [local_episode//1,
- episode_file/2,
- open_episode_locally/1,
- open_episode_online/1]).
-
-:- use_module(library(dcg/basics)).
-:- use_module(atom_dcg).
-:- use_module(cfg).
-
-local_episode_prefix --> string(_), "Detective_Conan_-_".
-local_episode_prefix --> string(_), "Detective Conan - ".
-local_episode_prefix --> string(_), "Detective-Conan-".
-local_episode_prefix --> string(_), "detective_conan_".
-
-nondigit --> [C], !, { \+ code_type(C, digit) }.
-nondigit --> [].
-
-zeroes --> "0".
-zeroes --> "0", zeroes.
-
-paddedint(N) --> integer(N).
-paddedint(N) --> zeroes, integer(N).
-
-local_episode -->
- local_episode(_).
-local_episode(N) -->
- local_episode_prefix, paddedint(N), nondigit, string(_).
-local_episode(N) -->
- local_episode_prefix, paddedint(First), "-", paddedint(Last), nondigit, string(_),
- { Second is First + 1, between(Second, Last, N) }.
-
-% Find episode on disk.
-
-episode_file(N, F) :-
- get_root(R),
- get_glob(G),
- atomic_list_concat([R, '/', G], G1),
- expand_file_name(G1, F1),
- ( nonvar(N)
- -> include(atom_phrase(local_episode(N)), F1, [F|_])
- ; include(atom_phrase(local_episode), F1, F2),
- member(F, F2),
- atom_phrase(local_episode(N), F)
- ).
-
-% Open episode.
-
-episode_url(N) --> { with_codes(get_url(U)) }, string(U), integer(N).
-open_episode_locally(N) :- episode_file(N, F), !, win_shell(open, F).
-open_episode_online(N) :- atom_phrase(episode_url(N), U), win_shell(open, U).
diff --git a/pl/track_episodes.pl b/pl/track_episodes.pl
deleted file mode 100644
index 1973a75..0000000
--- a/pl/track_episodes.pl
+++ /dev/null
@@ -1,112 +0,0 @@
-:- module(track_episodes, [update_tracked_episodes/0,
- toggle_episode/1,
- forget_episode/1,
- most_recently_watched/1,
- watched/1,
- next_unwatched/2,
- previous_unwatched/2]).
-
-:- use_module(library(dcg/basics)).
-:- use_module(library(registry)).
-:- use_module(library(persistency)).
-:- use_module(local_episodes).
-
-attach :-
- absolute_file_name('track_episodes.db', F, [access(write)]),
- db_attach(F, []).
-
-detach :-
- db_detach.
-
-:- persistent episode_watched(episode:integer, watched:atom).
-
-% Helpers.
-
-swapbc(P, A, B, C, D) :- call(P, A, C, B, D).
-compound(F, A, B, T) :- T =.. [F, A, B].
-
-% Get data from registry.
-
-file(N) --> "File Name ", integer(N).
-
-pos(N) --> "File Position ", integer(N).
-
-mpc_reg(Phrase, Value) :-
- phrase(Phrase, Name),
- atom_codes(Name1, Name),
- registry_get_key(current_user/software/'mpc-hc'/'mpc-hc'/settings,
- Name1, Value).
-
-% Create episode-position list.
-
-epipos(L) :- epipos_x(0, L).
-
-epipos_x(N, L) :-
- ( catch((mpc_reg(file(N), File),
- mpc_reg(pos(N), Pos)),
- _,
- fail)
- -> M is N + 1,
- ( atom_codes(File, File1),
- findall(Ep, phrase(local_episode(Ep), File1), Eps)
- -> atom_codes(Pos, Pos1),
- phrase(integer(Pos2), Pos1),
- maplist(swapbc(compound, '-', Pos2), Eps, Eps1),
- append(Eps1, T, L),
- epipos_x(M, T)
- ; epipos_x(M, L))
- ; L = []
- ).
-
-considered_watched(_-Pos) :-
- Pos > 11000000000.
-
-% Update database.
-
-update_tracked_episodes :-
- epipos(All),
- include(considered_watched, All, Watched),
- maplist(add, Watched).
-
-add(Ep-_) :- episode_watched(Ep, _), !.
-add(Ep-_) :- assert_episode_watched(Ep, true).
-
-forget_episode(Ep) :-
- retractall_episode_watched(Ep, true),
- retractall_episode_watched(Ep, false).
-
-toggle_episode(Ep) :-
- episode_watched(Ep, true), !,
- retractall_episode_watched(Ep, true),
- assert_episode_watched(Ep, false).
-
-toggle_episode(Ep) :-
- episode_watched(Ep, false), !,
- retractall_episode_watched(Ep, false),
- assert_episode_watched(Ep, true).
-
-toggle_episode(Ep) :-
- assert_episode_watched(Ep, true).
-
-% Utility.
-
-most_recently_watched(Ep) :-
- findall(N, episode_watched(N, true), All),
- last(All, Ep).
-
-watched(Ep) :-
- episode_watched(Ep, true).
-
-next_unwatched(Ep0, Ep) :-
- Ep1 is Ep0 + 1,
- ( \+ watched(Ep1)
- -> Ep = Ep1
- ; next_unwatched(Ep1, Ep)
- ).
-
-previous_unwatched(Ep0, Ep) :-
- Ep1 is Ep0 - 1,
- ( \+ watched(Ep1)
- -> Ep = Ep1
- ; previous_unwatched(Ep1, Ep)
- ).