Restructure project

This commit is contained in:
luxick
2017-04-17 16:55:33 +02:00
parent 66955c94af
commit b7ee25d224
28 changed files with 626 additions and 559 deletions

19
README.rst Normal file
View File

@@ -0,0 +1,19 @@
# Card Vault
A desktop application for building and organizing MTG card libraries and decks.
## Features
* Online card search
* Create a library of owned cards
* Import and Export Libraries
## TODO
* Organize cards in library
* Build decklists from cards in collection
* Want lists
* Full offline functionality
### Maybe
* Create fancy exports of decks and wants
* Display prices for cards

2
bin/cardvault Normal file
View File

@@ -0,0 +1,2 @@
from cardvault import application
application.main()

0
cardvault/__init__.py Normal file
View File

263
cardvault/application.py Normal file
View File

@@ -0,0 +1,263 @@
import sys
try:
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Pango
from gi.repository import GdkPixbuf
except ImportError as ex:
print("Couldn't import GTK dependencies. Make sure you "
"installed the PyGTK package and %s module." % ex.name)
sys.exit(-1)
import os
import copy
import re
from cardvault import handlers
from cardvault import util
from cardvault import search_funct
from cardvault import lib_funct
class Application:
# ---------------------------------Initialize the Application----------------------------------------------
def __init__(self):
# Load ui files
self.ui = Gtk.Builder()
self.ui.add_from_file(util.get_ui_filename("mainwindow.glade"))
self.ui.add_from_file(util.get_ui_filename("overlays.glade"))
self.ui.add_from_file(util.get_ui_filename("search.glade"))
self.ui.add_from_file(util.get_ui_filename("library.glade"))
self.current_page = None
self.unsaved_changes = False
not_found = self.ui.get_object("pageNotFound")
self.pages = {
"search": self.ui.get_object("searchView"),
"library": self.ui.get_object("libraryView"),
"decks": not_found
}
# Load configuration file
self.configfile = util.get_root_filename("config.json")
self.config = util.parse_config(self.configfile, util.default_config)
util.LOG_LEVEL = self.config["log_level"]
# Load data from cache path
self.image_cache = util.reload_image_cache(util.CACHE_PATH + "images/")
self.precon_icons = util.reload_preconstructed_icons(util.CACHE_PATH + "icons/")
self.mana_icons = util.load_mana_icons(os.path.dirname(__file__) + "/resources/mana/")
self.sets = util.load_sets(util.get_root_filename("sets"))
self.library = None
self.tags = None
self.load_library()
self.handlers = handlers.Handlers(self)
self.ui.connect_signals(self.handlers)
search_funct.init_search_view(self)
lib_funct.init_library_view(self)
self.ui.get_object("mainWindow").connect('delete-event', Gtk.main_quit)
self.ui.get_object("mainWindow").show_all()
self.push_status("Card Vault ready.")
view_menu = self.ui.get_object("viewMenu")
start_page = [page for page in view_menu.get_children() if page.get_name() == util.START_PAGE]
start_page[0].activate()
def push_status(self, msg):
status_bar = self.ui.get_object("statusBar")
status_bar.pop(0)
status_bar.push(0, msg)
def show_card_details(self, card):
builder = Gtk.Builder()
builder.add_from_file(util.get_ui_filename("detailswindow.glade"))
builder.add_from_file(util.get_ui_filename("overlays.glade"))
window = builder.get_object("cardDetails")
window.set_title(card.name)
# Card Image
container = builder.get_object("imageContainer")
pixbuf = self.get_card_image(card, 63 * 5, 88 * 5)
image = Gtk.Image().new_from_pixbuf(pixbuf)
container.add(image)
# Name
builder.get_object("cardName").set_text(card.name)
# Types
supertypes = ""
if card.subtypes is not None:
supertypes = " - " + " ".join(card.subtypes)
types = " ".join(card.types) + supertypes
builder.get_object("cardTypes").set_text(types)
# Rarity
builder.get_object("cardRarity").set_text(card.rarity if card.rarity else "")
# Release
builder.get_object("cardReleaseDate").set_text(card.release_date if card.release_date else "")
# Set
builder.get_object("cardSet").set_text(card.set_name)
# Printings
prints = []
for set in card.printings:
prints.append(self.sets[set].name)
builder.get_object("cardPrintings").set_text(", ".join(prints))
# Legalities
grid = builder.get_object("legalitiesGrid")
rows = 1
for legality in card.legalities if card.legalities else {}:
date_label = Gtk.Label()
date_label.set_halign(Gtk.Align.END)
text_label = Gtk.Label()
text_label.set_line_wrap_mode(Pango.WrapMode.WORD)
text_label.set_line_wrap(True)
text_label.set_halign(Gtk.Align.END)
color = self.config['legality_colors'][legality["legality"]]
date_label.set_markup("<span fgcolor=\""+color+"\">" + legality["format"] + ":" + "</span>")
text_label.set_markup("<span fgcolor=\""+color+"\">" + legality["legality"] + "</span>")
grid.attach(date_label, 0, rows + 2, 1, 1)
grid.attach(text_label, 1, rows + 2, 1, 1)
rows += 1
grid.show_all()
# Rulings
if card.rulings:
grid = builder.get_object("rulesGrid")
rows = 1
for rule in card.rulings:
date_label = Gtk.Label(rule["date"])
text_label = Gtk.Label(rule["text"])
text_label.set_line_wrap_mode(Pango.WrapMode.WORD)
text_label.set_line_wrap(True)
text_label.set_justify(Gtk.Justification.LEFT)
text_label.set_halign(Gtk.Align.START)
grid.attach(date_label, 0, rows+2, 1, 1)
grid.attach(text_label, 1, rows+2, 1, 1)
rows += 1
grid.show_all()
else:
builder.get_object("ruleBox").set_visible(False)
window.show_all()
def eval_key_pressed(widget,event):
key, modifier = Gtk.accelerator_parse('Escape')
keyval = event.keyval
if keyval == key:
window.destroy()
window.connect("key-press-event", eval_key_pressed)
def show_question_dialog(self, title, message):
dialog = Gtk.MessageDialog(self.ui.get_object("mainWindow"), 0, Gtk.MessageType.WARNING,
Gtk.ButtonsType.YES_NO, title)
dialog.format_secondary_text(message)
response = dialog.run()
dialog.destroy()
return response
def show_message(self, title, message):
dialog = Gtk.MessageDialog(self.ui.get_object("mainWindow"), 0, Gtk.MessageType.INFO,
Gtk.ButtonsType.OK, title)
dialog.format_secondary_text(message)
dialog.run()
dialog.destroy()
def save_library(self):
# Save library file
util.save_file(util.get_root_filename("library"), self.library)
# Save tags file
util.save_file(util.get_root_filename("tags"), self.tags)
self.unsaved_changes = False
self.push_status("Library saved")
def load_library(self):
# Load library file
self.library = util.load_file(util.get_root_filename("library"))
# Load tags file
self.tags = util.load_file(util.get_root_filename("tags"))
self.push_status("Library loaded")
def get_untagged_cards(self):
lib = copy.copy(self.library)
for ids in self.tags.values():
for card_id in ids:
try:
del lib[card_id]
except KeyError:
pass
return lib
def get_tagged_cards(self, tag):
if not tag:
return self.library
else:
lib = {}
for card_id in self.tags[tag]:
lib[card_id] = self.library[card_id]
return lib
def tag_card(self, card, tag):
list = self.tags[tag]
list.append(card.multiverse_id)
self.unsaved_changes = True
def add_tag(self, tag):
self.tags[tag] = []
self.push_status("Added Tag \"" + tag + "\"")
self.unsaved_changes = True
def remove_tag(self, tag):
del self.tags[tag]
self.push_status("Removed Tag \"" + tag + "\"")
self.unsaved_changes = True
def add_card_to_lib(self, card, tag=None):
if tag is not None:
self.tag_card(card, tag)
self.library[card.multiverse_id] = card
self.push_status(card.name + " added to library")
self.unsaved_changes = True
def remove_card_from_lib(self, card):
del self.library[card.multiverse_id]
self.push_status(card.name + " removed from library")
self.unsaved_changes = True
def get_card_image(self, card, sizex, sizey):
# Try using file from local cache, or load online
try:
pixbuf = self.image_cache[card.multiverse_id]
except KeyError as err:
util.log("No local image for " + card.name + ". Loading from " + card.image_url, util.LogLevel.Info)
pixbuf = util.load_card_image_online(card, sizex, sizey)
self.image_cache[card.multiverse_id] = pixbuf
return pixbuf
def get_mana_icons(self, mana_string):
if not mana_string:
util.log("No mana string provided", util.LogLevel.Warning)
return
icon_list = re.findall("{(.*?)}", mana_string)
icon_name = "_".join(icon_list)
try:
icon = self.precon_icons[icon_name]
except KeyError:
icon = util.create_mana_icons(self.mana_icons, mana_string)
self.precon_icons[icon_name] = icon
return icon
def main():
win = Application()
Gtk.main()

View File

@@ -1,6 +1,5 @@
import gi
import util
import logger
from cardvault import util
from gi.repository import Gtk, GdkPixbuf, Gdk
import time
gi.require_version('Gtk', '3.0')
@@ -8,7 +7,7 @@ gi.require_version('Gdk', '3.0')
class CardList(Gtk.ScrolledWindow):
def __init__(self, with_filter):
def __init__(self, with_filter, app):
Gtk.ScrolledWindow.__init__(self)
self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
self.set_hexpand(True)
@@ -16,6 +15,7 @@ class CardList(Gtk.ScrolledWindow):
self.filtered = with_filter
self.lib = {}
self.app = app
# Columns are these:
# 0 Multiverse ID
@@ -136,11 +136,16 @@ class CardList(Gtk.ScrolledWindow):
if card.multiverse_id is not None:
if util.library.__contains__(card_id) and colorize:
if self.app.library.__contains__(card_id) and colorize:
color = util.card_view_colors["owned"]
else:
color = util.card_view_colors["unowned"]
if card.type == "Land":
mana_cost = None
else:
mana_cost = self.app.get_mana_icons(card.mana_cost)
item =[
card.multiverse_id,
card.name,
@@ -150,13 +155,13 @@ class CardList(Gtk.ScrolledWindow):
card.power,
card.toughness,
", ".join(card.printings),
util.get_mana_icons(card.mana_cost),
mana_cost,
card.cmc,
card.set_name,
color]
self.store.append(item)
end = time.time()
logger.log("Time to build Table: " + str(round(end - start, 3)), logger.LogLevel.Info)
util.log("Time to build Table: " + str(round(end - start, 3)), util.LogLevel.Info)
if self.filtered:
self.list.set_model(self.filter_and_sort)
self.list.thaw_child_notify()

View File

@@ -1,25 +0,0 @@
import gi
import os
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
from gi.repository import Gdk
# Title of the Program Window
application_title = "Card Vault v0.5"
# Path of image cache
cache_path = os.path.dirname(__file__) + "/.cache/"
image_cache_path = os.path.dirname(__file__) + "/.cache/images/"
icon_cache_path = os.path.dirname(__file__) + "/.cache/icons/"
# Colors to use in the Application
green_color = Gdk.color_parse('#87ff89')
red_color = Gdk.color_parse('#ff6d6d')
# When True Search view will list a card multiple times for each set they appear in
show_from_all_sets = True
start_page = "search"
log_level = 3

View File

@@ -1,11 +1,14 @@
import gi
import config
import lib_funct
import search_funct
import util
import datetime
import os
from gi.repository import Gtk
gi.require_version('Gtk', '3.0')
from cardvault import lib_funct
from cardvault import search_funct
from cardvault import util
class Handlers:
def __init__(self, app):
@@ -14,14 +17,44 @@ class Handlers:
# ----------------Main Window-----------------
def do_save_library(self, item):
util.save_library()
self.app.save_library()
def do_export_library(self, item):
util.export_library()
dialog = Gtk.FileChooserDialog("Export Library", self.app.ui.get_object("mainWindow"),
Gtk.FileChooserAction.SAVE,
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
dialog.set_current_name("mtg_export-" + datetime.datetime.now().strftime("%Y-%m-%d"))
dialog.set_current_folder(os.path.expanduser("~"))
response = dialog.run()
if response == Gtk.ResponseType.OK:
# prepare export file
file = {"library": self.app.library, "tags": self.app.tags}
util.export_library(dialog.get_filename, file)
dialog.destroy()
def do_import_library(self, item):
util.import_library()
self.app.current_page.emit('show')
# Show file picker dialog for import
dialog = Gtk.FileChooserDialog("Import Library", self.app.ui.get_object("mainWindow"),
Gtk.FileChooserAction.OPEN,
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
dialog.set_current_folder(os.path.expanduser("~"))
response = dialog.run()
if response == Gtk.ResponseType.OK:
# Show confirmation message
override_question = self.app.show_question_dialog("Import Library",
"Importing a library will override your current library. "
"Proceed?")
if override_question == Gtk.ResponseType.YES:
(library, tags) = util.import_library(dialog.get_filename())
self.app.library = library
self.app.tags = tags
# Cause current page to reload with imported data
self.app.current_page.emit('show')
dialog.destroy()
def on_view_changed(self, item):
if item.get_active():
@@ -34,22 +67,24 @@ class Handlers:
container.show_all()
self.app.current_page.emit('show')
app_title = new_page.get_name() + " - " + config.application_title
app_title = new_page.get_name() + " - " + util.APPLICATION_TITLE
self.app.ui.get_object("mainWindow").set_title(app_title)
def do_delete_event(self, arg1, arg2):
if util.unsaved_changes:
response = util.show_question_dialog("Unsaved Changes", "You have unsaved changes in your library. "
if self.app.unsaved_changes:
response = self.app.show_question_dialog("Unsaved Changes", "You have unsaved changes in your library. "
"Save before exiting?")
if response == Gtk.ResponseType.YES:
util.save_library()
self.app.save_library()
# ----------------Search-----------------
def do_search_cards(self, sender):
search_term = self.app.ui.get_object("searchEntry").get_text()
results = search_funct.search_cards(search_term)
filters = search_funct.get_filters(self.app)
results = search_funct.search_cards(search_term, filters)
card_list = self.app.ui.get_object("searchResults").get_child()
card_list.update(results, colorize=True)
@@ -72,10 +107,10 @@ class Handlers:
tree_iter = model.get_iter(path)
card_id = model.get_value(tree_iter, 0)
card = card_view.lib[card_id]
search_funct.add_to_library(card)
self.app.add_card_to_lib(card)
search_funct.reload_serach_view(self.app)
#----------------Library-----------------
# ----------------Library-----------------
def do_reload_library(self, view):
lib_funct.reload_library(self.app)
@@ -103,11 +138,11 @@ class Handlers:
selected_cards = card_view.get_selected_cards()
tag = entry.get_text()
if tag != "":
lib_funct.tag_cards(selected_cards, tag)
lib_funct.tag_cards(selected_cards, tag, self.app)
lib_funct.reload_library(self.app, tag)
entry.set_text("")
def on_drag_data_received(self, widget, drag_context, x,y, data,info, time):
def on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
print("drag received")
def on_tag_selected(self, selection, path, column):
@@ -119,7 +154,7 @@ class Handlers:
# Handlers for TreeViews etc. wich have been not added by Glade
#----------------Search-----------------
# ----------------Search-----------------
def on_search_card_selected(self, tree, row_no, column):
(model, path_list) = tree.get_selection().get_selected_rows()
@@ -150,5 +185,3 @@ class Handlers:
card_list = self.app.ui.get_object("libraryContainer").get_child()
card = card_list.lib[card_id]
self.app.show_card_details(card)

View File

@@ -1,5 +1,4 @@
import cardlist
import util
from cardvault import cardlist
import gi
gi.require_version('Gtk', '3.0')
@@ -7,7 +6,7 @@ gi.require_version('Gtk', '3.0')
def init_library_view(app):
# Create Tree View for library
container = app.ui.get_object("libraryContainer")
card_list = cardlist.CardList(True)
card_list = cardlist.CardList(True, app)
card_list.set_name("libScroller")
card_list.list.connect("row-activated", app.handlers.on_library_card_selected)
container.add(card_list)
@@ -20,10 +19,10 @@ def init_library_view(app):
def reload_library(app, tag=None):
if tag == "untagged":
lib = util.get_untagged_cards()
lib = app.get_untagged_cards()
tag = None
else:
lib = util.get_library(tag)
lib = app.get_tagged_cards(tag)
reload_tag_list(app, tag)
card_tree = app.ui.get_object("libraryContainer").get_child()
if lib:
@@ -35,9 +34,8 @@ def reload_library(app, tag=None):
app.ui.get_object("noResults").set_visible(True)
def add_new_tag(name, app):
util.add_tag(name)
app.add_tag(name)
reload_tag_list(app, True)
@@ -46,17 +44,17 @@ def reload_tag_list(app, preserve=False):
(path, column) = tree.get_cursor()
store = tree.get_model()
store.clear()
for tag, ids in util.tags.items():
for tag, ids in app.tags.items():
store.append([tag, tag + " (" + str(len(ids)) + ")"])
if preserve:
tree.set_cursor(path if path else 0)
def tag_cards(card_list, tag):
def tag_cards(card_list, tag, app):
# Check if tag exist and create if necessary
if not util.tags.__contains__(tag):
util.add_tag(tag)
if not app.tags.__contains__(tag):
app.add_tag(tag)
for card in card_list.values():
if not util.tags[tag].__contains__(card.multiverse_id):
util.tag_card(card, tag)
if not app.tags[tag].__contains__(card.multiverse_id):
app.tag_card(card, tag)

View File

@@ -1,14 +0,0 @@
import config
import enum
class LogLevel(enum.Enum):
Error = 1
Warning = 2
Info = 3
def log(message, log_level):
if log_level.value <= config.log_level:
level_string = "[" + log_level.name + "] "
print(level_string + message)

View File

@@ -1,11 +0,0 @@
from urllib import request
from urllib.error import URLError, HTTPError
from mtgsdk import Set
def net_load_sets():
try:
sets = Set.all()
except:
return ""
return sets

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -1,8 +1,6 @@
import gi
import util
import config
import cardlist
import logger
from cardvault import util
from cardvault import cardlist
from gi.repository import Gtk, Gdk
from mtgsdk import Card
from urllib.error import URLError, HTTPError
@@ -13,9 +11,9 @@ def init_search_view(app):
# set mana icons on filter buttons
buttons = [x for x in app.ui.get_object("manaFilterGrid").get_children()
if isinstance(x, Gtk.ToggleButton)]
_init_mana_buttons(buttons)
_init_mana_buttons(app, buttons)
# set auto completion for filter entry
_init_set_entry(app.ui.get_object("setEntry"))
_init_set_entry(app, app.ui.get_object("setEntry"))
# Fill rarity box
_init_combo_box(app.ui.get_object("rarityCombo"), util.rarity_dict.keys())
# Fill type box
@@ -30,15 +28,34 @@ def reload_serach_view(app):
pass
def add_to_library(card):
util.add_card_to_lib(card)
def get_filters(app):
output = {}
# Mana colors
color_list = []
# Go through mana color buttons an get the active filters
for button in app.ui.get_object("manaFilterGrid").get_children():
if isinstance(button, Gtk.ToggleButton):
if button.get_active():
color_list.append(button.get_name())
output["mana"] = ",".join(color_list)
# Rarity
combo = app.ui.get_object("rarityCombo")
output["rarity"] = _get_combo_value(combo)
# Type
combo = app.ui.get_object("typeCombo")
output["type"] = _get_combo_value(combo)
# Set
name = app.ui.get_object("setEntry").get_text()
output["set"] = ""
for set in app.sets.values():
if set.name == name:
output["set"] = set.code
return output
def search_cards(term):
logger.log("Starting online search for '" + term + "'", logger.LogLevel.Info)
# Load filters from UI
filters = _get_filters(util.app)
logger.log("Used Filters: " + str(filters), logger.LogLevel.Info)
def search_cards(term, filters):
util.log("Starting online search for '" + term + "'", util.LogLevel.Info)
util.log("Used Filters: " + str(filters), util.LogLevel.Info)
# Load card info from internet
try:
@@ -56,9 +73,9 @@ def search_cards(term):
if len(cards) == 0:
# TODO UI show no cards found
return
logger.log("Found " + str(len(cards)) + " cards", logger.LogLevel.Info)
util.log("Found " + str(len(cards)) + " cards", util.LogLevel.Info)
# Remove duplicate entries
if config.show_from_all_sets is False:
if util.SHOW_FROM_ALL_SETS is False:
cards = _remove_duplicates(cards)
# Pack results in a dictionary
@@ -70,7 +87,7 @@ def search_cards(term):
def _init_results_tree(app):
overlay = app.ui.get_object("searchResults")
card_list = cardlist.CardList(False)
card_list = cardlist.CardList(False, app)
card_list.set_name("resultsScroller")
card_list.list.connect("row-activated", app.handlers.on_search_card_selected)
card_list.selection.connect("changed", app.handlers.on_search_selection_changed)
@@ -91,31 +108,6 @@ def _init_combo_box(combo, list):
combo.set_active(0)
def _get_filters(app):
output = {}
# Mana colors
color_list = []
# Go through mana color buttons an get the active filters
for button in app.ui.get_object("manaFilterGrid").get_children():
if isinstance(button, Gtk.ToggleButton):
if button.get_active():
color_list.append(button.get_name())
output["mana"] = ",".join(color_list)
# Rarity
combo = app.ui.get_object("rarityCombo")
output["rarity"] = _get_combo_value(combo)
# Type
combo = app.ui.get_object("typeCombo")
output["type"] = _get_combo_value(combo)
# Set
name = app.ui.get_object("setEntry").get_text()
output["set"] = ""
for set in util.set_list:
if set.name == name:
output["set"] = set.code
return output
def _remove_duplicates(cards):
unique_cards = []
unique_names = []
@@ -133,15 +125,15 @@ def _get_combo_value(combo):
return value.replace("All", "")
def _init_mana_buttons(button_list):
def _init_mana_buttons(app, button_list):
for button in button_list:
image = Gtk.Image.new_from_pixbuf(util.create_mana_icons("{" + button.get_name() + "}"))
image = Gtk.Image.new_from_pixbuf(app.get_mana_icons("{" + button.get_name() + "}"))
button.set_image(image)
def _init_set_entry(entry):
def _init_set_entry(app, entry):
set_store = Gtk.ListStore(str, str)
for set in util.set_list:
for set in app.sets.values():
set_store.append([set.name, set.code])
completer = Gtk.EntryCompletion()
completer.set_model(set_store)

View File

@@ -1,34 +1,46 @@
import os
import datetime
import gi
import re
import config
import logger
import network
import enum
import copy
import json
from gi.repository import GdkPixbuf, Gtk
from PIL import Image as PImage
from urllib import request
import six.moves.cPickle as pickle
gi.require_version('Gtk', '3.0')
from mtgsdk import Set
from mtgsdk import MtgException
# Locally stored images for faster loading times
imagecache = {}
manaicons = {}
mana_icons_preconstructed = {}
# Title of the Program Window
APPLICATION_TITLE = "Card Vault"
set_list = []
set_dict = {}
# Program version
VERSION = "0.5.0"
# Card library object
library = {}
# Dictionary for tagged cards
tags = {}
# Path of image cache
CACHE_PATH = os.path.expanduser('~') + "/.cardvault/"
IMAGE_CACHE_PATH = os.path.expanduser('~') + "/.cardvault/images/"
ICON_CACHE_PATH = os.path.expanduser('~') + "/.cardvault/icons/"
status_bar = None
app = None
unsaved_changes = False
# When True Search view will list a card multiple times for each set they appear in
SHOW_FROM_ALL_SETS = True
START_PAGE = "search"
LOG_LEVEL = 1
default_config = {
"hide_duplicates_in_search": False,
"start_page": "search",
"log_level": 3,
"legality_colors": {
"Banned": "#C65642",
"Restricted": "#D39F30",
"Legal": "#62B62F"
}
}
legality_colors ={
"Banned": "#C65642",
@@ -52,264 +64,176 @@ rarity_dict = {
card_types = ["Creature", "Artifact", "Instant", "Enchantment", "Sorcery", "Land", "Planeswalker"]
def export_library():
dialog = Gtk.FileChooserDialog("Export Library", app.ui.get_object("mainWindow"),
Gtk.FileChooserAction.SAVE,
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
dialog.set_current_name("mtg_export-" + datetime.datetime.now().strftime("%Y-%m-%d"))
dialog.set_current_folder(os.path.expanduser("~"))
response = dialog.run()
if response == Gtk.ResponseType.OK:
# prepare export file
export = {"library": library, "tags": tags}
try:
pickle.dump(export, open(dialog.get_filename(), 'wb'))
app.push_status("Library exported to \"" + dialog.get_filename() + "\"")
logger.log("Library exported to \"" + dialog.get_filename() + "\"", logger.LogLevel.Info)
except OSError as err:
show_message("Error", err.strerror)
logger.log(str(err), logger.LogLevel.Error)
dialog.destroy()
class LogLevel(enum.Enum):
Error = 1
Warning = 2
Info = 3
def import_library():
dialog = Gtk.FileChooserDialog("Import Library", app.ui.get_object("mainWindow"),
Gtk.FileChooserAction.OPEN,
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
dialog.set_current_folder(os.path.expanduser("~"))
response = dialog.run()
if response == Gtk.ResponseType.OK:
override_question = show_question_dialog("Import Library",
"Importing a library will override your current library. "
"Proceed?")
if override_question == Gtk.ResponseType.YES:
try:
imported = pickle.load(open(dialog.get_filename(), 'rb'))
except pickle.UnpicklingError as err:
show_message("Error", "Imported file is invalid")
logger.log(str(err) + " while importing", logger.LogLevel.Error)
dialog.destroy()
return
# Check imported file
try:
global library
library = imported["library"]
global tags
tags = imported["tags"]
except KeyError as err:
logger.log("Invalid library format " + str(err), logger.LogLevel.Warning)
# Try fallback method
library.clear()
for id, card in imported.items():
library[id] = card
save_library()
app.push_status("Library imported")
logger.log("Library imported", logger.LogLevel.Info)
dialog.destroy()
def log(message, log_level):
if log_level.value <= LOG_LEVEL:
level_string = "[" + log_level.name + "] "
print(level_string + message)
def save_library():
if not os.path.exists(config.cache_path):
os.makedirs(config.cache_path)
lib_path = config.cache_path + "library"
tag_path = config.cache_path + "tags"
# Serialize library object using pickle
def parse_config(filename, default):
config = copy.copy(default)
try:
pickle.dump(library, open(lib_path, 'wb'))
pickle.dump(tags, open(tag_path, 'wb'))
except OSError as err:
show_message("Error", err.strerror)
logger.log(str(err), logger.LogLevel.Error)
with open(filename) as configfile:
loaded_config = json.load(configfile)
if 'legality_colors' in config and 'legality_colors' in loaded_config:
# Need to prevent nested dict from being overwritten with an incomplete dict
config['legality_colors'].update(loaded_config['legality_colors'])
loaded_config['legality_colors'] = config['legality_colors']
config.update(loaded_config)
except IOError:
# Will just use the default config
# and create the file for manual editing
save_config(config, filename)
except ValueError:
# There's a syntax error in the config file
log("Syntax error wihle parsing config file", LogLevel.Error)
return
global unsaved_changes
unsaved_changes = False
app.push_status("Library saved.")
logger.log("library saved", logger.LogLevel.Info)
return config
def load_library():
lib_path = config.cache_path + "library"
library.clear()
def save_config(config_dict, filename):
path = os.path.dirname(filename)
if not os.path.isdir(path):
os.mkdir(path)
if os.path.isfile(lib_path):
# Deserialize using pickle
with open(filename, 'wb') as configfile:
configfile.write(json.dumps(config_dict, sort_keys=True,
indent=4, separators=(',', ': ')).encode('utf-8'))
def get_root_filename(filename):
return os.path.expanduser(os.path.join('~', '.cardvault', filename))
def get_ui_filename(filename):
return os.path.expanduser(os.path.join(os.path.dirname(__file__), 'gui', filename))
def reload_image_cache(path):
cache = {}
if not os.path.isdir(path):
os.mkdir(path)
imagefiles = os.listdir(path)
for imagefile in imagefiles:
try:
library_loaded = pickle.load(open(lib_path, 'rb'))
for id, card in library_loaded.items():
library[id] = card
pixbuf = GdkPixbuf.Pixbuf.new_from_file(path + imagefile)
# Strip filename extension
imagename = os.path.splitext(imagefile)[0]
cache[imagename] = pixbuf
except OSError as err:
show_message("Error", err.strerror)
logger.log(str(err), logger.LogLevel.Error)
else:
save_library()
logger.log("No Library file found, creating new one", logger.LogLevel.Warning)
log("Error loading image: " + str(err), LogLevel.Error)
return cache
def load_tags():
tag_path = config.cache_path + "tags"
tags.clear()
if not os.path.isfile(tag_path):
save_library()
logger.log("No tags file found, creating new one", logger.LogLevel.Warning)
try:
tags_loaded = pickle.load(open(tag_path, 'rb'))
for tag, ids in tags_loaded.items():
tags[tag] = ids
except OSError as err:
show_message("Error", err.strerror)
logger.log(str(err), logger.LogLevel.Error)
def reload_preconstructed_icons(path):
cache = {}
if not os.path.exists(path):
os.makedirs(path)
def load_sets():
path = config.cache_path + "sets"
if not os.path.isfile(path):
# use mtgsdk api to retrieve al list of all sets
new_sets = network.net_load_sets()
if new_sets == "":
show_message("API Error", "Could not retrieve Set infos")
return
# Serialize the loaded data to a file
pickle.dump(new_sets, open(path, 'wb'))
# Deserialize set data from local file
sets = pickle.load(open(path, 'rb'))
# Sort the loaded sets based on the sets name
for set in sorted(sets, key=lambda x: x.name):
set_list.append(set)
set_dict[set.code] = set
def reload_image_cache():
if not os.path.exists(config.image_cache_path):
os.makedirs(config.image_cache_path)
# return array of images
imageslist = os.listdir(config.image_cache_path)
imagecache.clear()
for image in imageslist:
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file(config.image_cache_path + image)
imagecache[image] = pixbuf
except OSError as err:
print("Error loading image: " + str(err))
def reload_preconstructed_icons():
if not os.path.exists(config.icon_cache_path):
os.makedirs(config.icon_cache_path)
icon_list = os.listdir(config.icon_cache_path)
mana_icons_preconstructed.clear()
for icon in icon_list:
list = re.findall("{(.*?)}", str(icon))
iconfiles = os.listdir(path)
for file in iconfiles:
# Split filename into single icon names and remove extension
without_ext = file.split(".")[0]
list = without_ext.split("_")
# Compute size of the finished icon
pic_width = len(list) * 105
pic_height = 105
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file(config.icon_cache_path + icon)
pixbuf = GdkPixbuf.Pixbuf.new_from_file(ICON_CACHE_PATH + file)
pixbuf = pixbuf.scale_simple(pic_width / 5, pic_height / 5, GdkPixbuf.InterpType.HYPER)
mana_icons_preconstructed[icon] = pixbuf
# Set name for icon
iconname = "_".join(list)
cache[iconname] = pixbuf
except OSError as err:
print("Error loading icon: " + str(err))
log("Error loading image: " + str(err), LogLevel.Error)
return cache
# endregion
def get_library(tag=None):
if tag is None or tag == "All":
return library
else:
lib = {}
for card_id in tags[tag]:
lib[card_id] = library[card_id]
return lib
def get_untagged_cards():
lib = copy.copy(library)
for ids in tags.values():
for card_id in ids:
try:
del lib[card_id]
except KeyError:
pass
return lib
def tag_card(card, tag):
list = tags[tag]
list.append(card.multiverse_id)
global unsaved_changes
unsaved_changes = True
def add_tag(tag):
tags[tag] = []
app.push_status("Added Tag \"" + tag + "\"")
global unsaved_changes
unsaved_changes = True
def remove_tag(tag):
del tags[tag]
app.push_status("Removed Tag \"" + tag + "\"")
global unsaved_changes
unsaved_changes = True
def add_card_to_lib(card, tag=None):
if tag is not None:
tag_card(card, tag)
library[card.multiverse_id] = card
app.push_status(card.name + " added to library")
global unsaved_changes
unsaved_changes = True
def remove_card_from_lib(card):
del library[card.multiverse_id]
app.push_status(card.name + " removed from library")
global unsaved_changes
unsaved_changes = True
def show_question_dialog(title, message):
dialog = Gtk.MessageDialog(app.ui.get_object("mainWindow"), 0, Gtk.MessageType.WARNING,
Gtk.ButtonsType.YES_NO, title)
dialog.format_secondary_text(message)
response = dialog.run()
dialog.destroy()
return response
def show_message(title, message):
dialog = Gtk.MessageDialog(app.ui.get_object("mainWindow"), 0, Gtk.MessageType.INFO,
Gtk.ButtonsType.OK, title)
dialog.format_secondary_text(message)
dialog.run()
dialog.destroy()
def load_mana_icons():
path = os.path.dirname(__file__) + "/resources/mana/"
def load_mana_icons(path):
if not os.path.exists(path):
print("ERROR: Directory for mana icons not found")
log("Directory for mana icons not found " + path, LogLevel.Error)
return
# return array of icons
imagelist = os.listdir(path)
manaicons.clear()
for image in imagelist:
img = PImage.open(path + image)
manaicons[os.path.splitext(image)[0]] = img
icons = {}
filenames = os.listdir(path)
for file in filenames:
img = PImage.open(path + file)
# Strip file extension
name = os.path.splitext(file)[0]
icons[name] = img
return icons
def load_sets(filename):
if not os.path.isfile(filename):
# use mtgsdk api to retrieve al list of all sets
try:
sets = Set.all()
except MtgException as err:
log(str(err), LogLevel.Error)
return
# Serialize the loaded data to a file
pickle.dump(sets, open(filename, 'wb'))
# Deserialize set data from local file
sets = pickle.load(open(filename, 'rb'))
# Sort the loaded sets based on the sets name
output = {}
for set in sorted(sets, key=lambda x: x.name):
output[set.code] = set
return output
def export_library(path, file):
try:
pickle.dump(file, open(path, 'wb'))
log("Library exported to \"" + path + "\"", LogLevel.Info)
except OSError as err:
log(str(err), LogLevel.Error)
def import_library(path):
try:
imported = pickle.load(open(path, 'rb'))
except pickle.UnpicklingError as err:
log(str(err) + " while importing", LogLevel.Error)
return
# Parse imported file
try:
library = imported["library"]
tags = imported["tags"]
except KeyError as err:
log("Invalid library format " + str(err), LogLevel.Error)
return
log("Library imported", LogLevel.Info)
return (library, tags)
def save_file(path, file):
if not os.path.exists(path):
os.makedirs(path)
# Serialize using cPickle
try:
pickle.dump(file, open(path, 'wb'))
except OSError as err:
log(str(err), LogLevel.Error)
return
log("Saved file " + path, LogLevel.Info)
def load_file(path):
if not os.path.isfile(path):
log(path + " does not exist", LogLevel.Error)
try:
loaded = pickle.load(open(path, 'rb'))
except OSError as err:
log(str(err), LogLevel.Error)
return
return loaded
def load_dummy_image(sizex, sizey):
@@ -320,38 +244,17 @@ def load_dummy_image(sizex, sizey):
def load_card_image_online(card, sizex, sizey):
url = card.image_url
if url is None:
print("No Image URL provided")
log("No Image URL for " + card.name, LogLevel.Warning)
return load_dummy_image(sizex, sizey)
filename = config.image_cache_path + card.multiverse_id.__str__() + ".PNG"
filename = IMAGE_CACHE_PATH + str(card.multiverse_id) + ".png"
request.urlretrieve(url, filename)
reload_image_cache()
return GdkPixbuf.Pixbuf.new_from_file_at_size(filename, sizex, sizey)
def load_card_image(card, sizex, sizey):
# Try loading from disk, if file exists
filename = str(card.multiverse_id) + ".PNG"
if imagecache.__contains__(filename):
pixbuf = imagecache[filename]
return pixbuf.scale_simple(sizex, sizey, GdkPixbuf.InterpType.BILINEAR)
else:
return load_card_image_online(card, sizex, sizey)
def get_mana_icons(mana_string):
if not mana_string:
return
try:
icon = mana_icons_preconstructed[mana_string.replace("/", "") + ".png"]
except KeyError:
icon = create_mana_icons(mana_string)
mana_icons_preconstructed[mana_string] = icon
return icon
def create_mana_icons(mana_string):
def create_mana_icons(icon_dict, mana_string):
# Convert the string to a List
list = re.findall("{(.*?)}", str(mana_string))
safe_string = mana_string.replace("/", "-")
list = re.findall("{(.*?)}", safe_string)
if len(list) == 0:
return
# Compute horizontal size for the final image
@@ -362,18 +265,22 @@ def create_mana_icons(mana_string):
# Go through all entries an add the correspondent icon to the final image
for icon in list:
xpos = poscounter * 105
loaded = manaicons.get(icon.replace("/", ""))
if loaded is None:
print("ERROR: No icon file named \"" + icon + "\" found.")
else:
image.paste(loaded, (xpos, 0))
try:
loaded = icon_dict[icon]
except KeyError as err:
log("No icon file named '" + icon + "' found.", LogLevel.Warning)
return
image.paste(loaded, (xpos, 0))
poscounter += 1
path = config.icon_cache_path + mana_string.replace("/", "") + ".png"
# Save Icon file
path = ICON_CACHE_PATH + "_".join(list) + ".png"
image.save(path)
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
pixbuf = pixbuf.scale_simple(image.width / 5, image.height / 5, GdkPixbuf.InterpType.HYPER)
except:
return
mana_icons_preconstructed[mana_string.replace("/", "") + ".png"] = pixbuf
return pixbuf
# endregion

View File

@@ -1,140 +0,0 @@
import config
import handlers
import util
import search_funct
import lib_funct
import gi
from gi.repository import Gtk, Pango
gi.require_version('Gtk', '3.0')
class MainWindow:
def __init__(self):
self.ui = Gtk.Builder()
self.ui.add_from_file("gui/mainwindow.glade")
self.ui.add_from_file("gui/overlays.glade")
self.ui.add_from_file("gui/search.glade")
self.ui.add_from_file("gui/library.glade")
window = self.ui.get_object("mainWindow")
self.current_page = None
util.app = self
not_found = self.ui.get_object("pageNotFound")
self.pages = {
"search": self.ui.get_object("searchView"),
"library": self.ui.get_object("libraryView"),
"decks": not_found
}
# Load local image Data
util.reload_image_cache()
util.reload_preconstructed_icons()
util.load_mana_icons()
util.load_sets()
util.load_library()
util.load_tags()
self.handlers = handlers.Handlers(self)
self.ui.connect_signals(self.handlers)
search_funct.init_search_view(self)
lib_funct.init_library_view(self)
window.connect('delete-event', Gtk.main_quit)
window.show_all()
self.push_status("Card Vault ready.")
view_menu = self.ui.get_object("viewMenu")
start_page = [page for page in view_menu.get_children() if page.get_name() == config.start_page]
start_page[0].activate()
def push_status(self, msg):
status_bar = self.ui.get_object("statusBar")
status_bar.pop(0)
status_bar.push(0, msg)
def show_card_details(self, card):
builder = Gtk.Builder()
builder.add_from_file("gui/detailswindow.glade")
builder.add_from_file("gui/overlays.glade")
window = builder.get_object("cardDetails")
window.set_title(card.name)
# Card Image
container = builder.get_object("imageContainer")
pixbuf = util.load_card_image(card, 63 * 5, 88 * 5)
image = Gtk.Image().new_from_pixbuf(pixbuf)
container.add(image)
# Name
builder.get_object("cardName").set_text(card.name)
# Types
supertypes = ""
if card.subtypes is not None:
supertypes = " - " + " ".join(card.subtypes)
types = " ".join(card.types) + supertypes
builder.get_object("cardTypes").set_text(types)
# Rarity
builder.get_object("cardRarity").set_text(card.rarity if card.rarity else "")
# Release
builder.get_object("cardReleaseDate").set_text(card.release_date if card.release_date else "")
# Set
builder.get_object("cardSet").set_text(card.set_name)
# Printings
prints = []
for set in card.printings:
prints.append(util.set_dict[set].name)
builder.get_object("cardPrintings").set_text(", ".join(prints))
# Legalities
grid = builder.get_object("legalitiesGrid")
rows = 1
for legality in card.legalities if card.legalities else {}:
date_label = Gtk.Label()
date_label.set_halign(Gtk.Align.END)
text_label = Gtk.Label()
text_label.set_line_wrap_mode(Pango.WrapMode.WORD)
text_label.set_line_wrap(True)
text_label.set_halign(Gtk.Align.END)
color = util.legality_colors[legality["legality"]]
date_label.set_markup("<span fgcolor=\""+color+"\">" + legality["format"] + ":" + "</span>")
text_label.set_markup("<span fgcolor=\""+color+"\">" + legality["legality"] + "</span>")
grid.attach(date_label, 0, rows + 2, 1, 1)
grid.attach(text_label, 1, rows + 2, 1, 1)
rows += 1
grid.show_all()
# Rulings
if card.rulings:
grid = builder.get_object("rulesGrid")
rows = 1
for rule in card.rulings:
date_label = Gtk.Label(rule["date"])
text_label = Gtk.Label(rule["text"])
text_label.set_line_wrap_mode(Pango.WrapMode.WORD)
text_label.set_line_wrap(True)
text_label.set_justify(Gtk.Justification.LEFT)
text_label.set_halign(Gtk.Align.START)
grid.attach(date_label, 0, rows+2, 1, 1)
grid.attach(text_label, 1, rows+2, 1, 1)
rows += 1
grid.show_all()
else:
builder.get_object("ruleBox").set_visible(False)
window.show_all()
def eval_key_pressed(widget,event):
key, modifier = Gtk.accelerator_parse('Escape')
keyval = event.keyval
if keyval == key:
window.destroy()
window.connect("key-press-event", eval_key_pressed)
win = MainWindow()
Gtk.main()

38
setup.py Executable file
View File

@@ -0,0 +1,38 @@
from setuptools import setup, find_packages
from cardvault import util
try:
LONG_DESCRIPTION = open("README.rst").read()
except IOError:
LONG_DESCRIPTION = __doc__
setup(
name=util.APPLICATION_TITLE,
version=util.VERSION,
packages=find_packages(),
# install_requires=['pygobject'],
package_data={'cardvault': ['resources/images/*', 'resources/mana/*', 'gui/*']},
author='luxick',
author_email='cardvoult@luxick.de',
description='Managing MTG card libraries and decks',
long_description=LONG_DESCRIPTION,
url='https://github.com/luxick/cardvault',
keywords='card manager, gtk, MTG, Magic the Gathering',
license="MIT",
entry_points={
'gui_scripts': [
'cardvault = cardvault.application:main',
]
},
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: End Users/Desktop',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'License :: OSI Approved :: MIT License',
]
)